Enterprise AI Infrastructure: Gateways, MLOps, and Production Architecture

Enterprise AI Infrastructure: Gateways, MLOps, and Production Architecture

Production-grade AI systems require sophisticated infrastructure that goes far beyond simply calling API endpoints. As enterprises transition from experimental pilots to production deployments, they must build comprehensive platforms that manage multiple AI providers, ensure reliability and performance, provide governance and security, and enable continuous integration and deployment of AI models. This article provides a detailed technical exploration of the infrastructure stack required for enterprise AI operations in 2026.

We will examine AI gateway platforms that abstract provider complexity, MLOps practices enabling continuous model delivery, multi-cloud architectures providing resilience and flexibility, and production deployment patterns proven in real-world enterprise environments. Throughout this discussion, we will provide concrete code examples in Node.js, Python, and C# demonstrating how to implement these infrastructure components.

The Enterprise AI Infrastructure Stack

Modern enterprise AI infrastructure consists of multiple layers, each providing essential capabilities for production operations. Understanding this layered architecture is critical for building systems that scale reliably.

At the foundation lies the data layer, encompassing data lakes, warehouses, and streaming platforms that provide AI systems with training data and operational context. Above this sits the model layer, including training infrastructure, model registries, and versioning systems. The orchestration layer manages workflow execution, resource allocation, and job scheduling. The serving layer handles model inference, load balancing, and scaling. The observability layer provides monitoring, logging, and alerting. Finally, the governance layer enforces policies, manages access control, and ensures compliance.

Each layer must integrate seamlessly with the others while maintaining clear separation of concerns. This modular architecture allows organizations to replace components as technology evolves without requiring complete system redesigns.

AI Gateway Platforms: Abstracting Provider Complexity

With 40% of enterprise applications now integrated with task-specific AI agents, organizations require robust infrastructure for managing access to multiple AI model providers. AI gateways serve as the critical abstraction layer between applications and AI providers, offering unified interfaces, intelligent routing, cost optimization, and comprehensive governance.

Core Gateway Capabilities

Enterprise AI gateways must provide several fundamental capabilities. Unified API interfaces allow applications to interact with multiple AI providers through a single, consistent API, eliminating the need to maintain provider-specific integration code. Dynamic provider routing enables automatic selection of optimal providers based on cost, latency, availability, and capability requirements. Intelligent failover mechanisms ensure high availability by automatically switching to backup providers when primary providers experience issues. Semantic caching reduces costs and latency by recognizing when requests are semantically similar to previous requests, even if not identical.

Budget management and cost controls provide hierarchical spending limits at team, project, and customer levels with real-time tracking preventing budget overruns. Security features include authentication, authorization, API key management, and audit logging satisfying compliance requirements. Observability capabilities deliver detailed metrics on usage patterns, provider performance, error rates, and cost analytics.

Implementing an AI Gateway in Node.js

Let us examine a production-ready AI gateway implementation in Node.js using Express and modern JavaScript patterns. This implementation demonstrates core gateway patterns including provider abstraction, request routing, caching, and observability.

// ai-gateway-server.js
import express from 'express';
import { createClient } from 'redis';
import winston from 'winston';
import { OpenAI } from 'openai';
import Anthropic from '@anthropic-ai/sdk';

// Initialize logger
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' }),
    new winston.transports.Console({ format: winston.format.simple() })
  ]
});

// Initialize Redis for caching
const redis = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379'
});
await redis.connect();

// Provider configurations
const providers = {
  openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
  anthropic: new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
};

// Cost tracking storage
const costTracker = new Map();

// Gateway configuration
const gatewayConfig = {
  defaultProvider: 'anthropic',
  maxRetries: 3,
  cacheTTL: 3600, // 1 hour
  budgetLimits: {
    daily: 1000.00,
    monthly: 25000.00
  },
  providerCosts: {
    'openai:gpt-4': { input: 0.03, output: 0.06 }, // per 1K tokens
    'anthropic:claude-sonnet-4': { input: 0.003, output: 0.015 }
  }
};

const app = express();
app.use(express.json());

// Middleware: Request logging
app.use((req, res, next) => {
  const requestId = crypto.randomUUID();
  req.requestId = requestId;
  logger.info({
    requestId,
    method: req.method,
    path: req.path,
    timestamp: new Date().toISOString()
  });
  next();
});

// Middleware: Budget checking
async function checkBudget(req, res, next) {
  const today = new Date().toISOString().split('T')[0];
  const dailyKey = `cost:daily:${today}`;
  const monthlyKey = `cost:monthly:${today.substring(0, 7)}`;
  
  const dailyCost = parseFloat(await redis.get(dailyKey) || '0');
  const monthlyCost = parseFloat(await redis.get(monthlyKey) || '0');
  
  if (dailyCost >= gatewayConfig.budgetLimits.daily) {
    return res.status(429).json({
      error: 'Daily budget limit exceeded',
      dailyLimit: gatewayConfig.budgetLimits.daily,
      currentSpend: dailyCost
    });
  }
  
  if (monthlyCost >= gatewayConfig.budgetLimits.monthly) {
    return res.status(429).json({
      error: 'Monthly budget limit exceeded',
      monthlyLimit: gatewayConfig.budgetLimits.monthly,
      currentSpend: monthlyCost
    });
  }
  
  req.budgetCheck = { dailyCost, monthlyCost, today };
  next();
}

// Generate cache key from request
function generateCacheKey(messages, model, temperature) {
  const normalized = JSON.stringify({
    messages: messages.map(m => ({ role: m.role, content: m.content })),
    model,
    temperature: temperature || 0
  });
  return `cache:${crypto.createHash('sha256').update(normalized).digest('hex')}`;
}

// Calculate token usage and cost
function calculateCost(provider, model, inputTokens, outputTokens) {
  const key = `${provider}:${model}`;
  const costs = gatewayConfig.providerCosts[key];
  if (!costs) return 0;
  
  return ((inputTokens / 1000) * costs.input) + ((outputTokens / 1000) * costs.output);
}

// Track cost
async function trackCost(cost, date) {
  const dailyKey = `cost:daily:${date}`;
  const monthlyKey = `cost:monthly:${date.substring(0, 7)}`;
  
  await redis.incrByFloat(dailyKey, cost);
  await redis.incrByFloat(monthlyKey, cost);
  await redis.expire(dailyKey, 86400 * 7); // Keep 7 days
  await redis.expire(monthlyKey, 86400 * 60); // Keep 60 days
}

// Call OpenAI provider
async function callOpenAI(messages, model, temperature, maxTokens) {
  const response = await providers.openai.chat.completions.create({
    model: model || 'gpt-4',
    messages,
    temperature: temperature || 0.7,
    max_tokens: maxTokens || 4096
  });
  
  return {
    content: response.choices[0].message.content,
    usage: response.usage,
    provider: 'openai',
    model: response.model
  };
}

// Call Anthropic provider
async function callAnthropic(messages, model, temperature, maxTokens) {
  const response = await providers.anthropic.messages.create({
    model: model || 'claude-sonnet-4-20250514',
    messages,
    temperature: temperature || 0.7,
    max_tokens: maxTokens || 4096
  });
  
  return {
    content: response.content[0].text,
    usage: {
      prompt_tokens: response.usage.input_tokens,
      completion_tokens: response.usage.output_tokens,
      total_tokens: response.usage.input_tokens + response.usage.output_tokens
    },
    provider: 'anthropic',
    model: response.model
  };
}

// Main completion endpoint with caching and failover
app.post('/v1/chat/completions', checkBudget, async (req, res) => {
  const startTime = Date.now();
  const { messages, model, temperature, max_tokens, provider } = req.body;
  
  try {
    // Check cache first
    const cacheKey = generateCacheKey(messages, model, temperature);
    const cached = await redis.get(cacheKey);
    
    if (cached) {
      logger.info({
        requestId: req.requestId,
        status: 'cache_hit',
        latency: Date.now() - startTime
      });
      
      return res.json({
        ...JSON.parse(cached),
        cached: true,
        requestId: req.requestId
      });
    }
    
    // Determine provider to use
    const targetProvider = provider || gatewayConfig.defaultProvider;
    let response;
    let attempts = 0;
    const providers_order = targetProvider === 'openai' 
      ? ['openai', 'anthropic'] 
      : ['anthropic', 'openai'];
    
    // Attempt with failover
    while (attempts < gatewayConfig.maxRetries) {
      try {
        const currentProvider = providers_order[attempts % providers_order.length];
        
        if (currentProvider === 'openai') {
          response = await callOpenAI(messages, model, temperature, max_tokens);
        } else {
          response = await callAnthropic(messages, model, temperature, max_tokens);
        }
        
        break; // Success, exit retry loop
      } catch (error) {
        attempts++;
        logger.error({
          requestId: req.requestId,
          attempt: attempts,
          error: error.message,
          provider: providers_order[(attempts - 1) % providers_order.length]
        });
        
        if (attempts >= gatewayConfig.maxRetries) {
          throw new Error('All provider attempts failed');
        }
        
        // Wait before retry with exponential backoff
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempts) * 1000));
      }
    }
    
    // Calculate and track cost
    const cost = calculateCost(
      response.provider,
      response.model,
      response.usage.prompt_tokens,
      response.usage.completion_tokens
    );
    
    await trackCost(cost, req.budgetCheck.today);
    
    // Cache the response
    const cacheValue = {
      content: response.content,
      usage: response.usage,
      provider: response.provider,
      model: response.model,
      cost
    };
    
    await redis.setEx(cacheKey, gatewayConfig.cacheTTL, JSON.stringify(cacheValue));
    
    // Log metrics
    logger.info({
      requestId: req.requestId,
      status: 'success',
      provider: response.provider,
      latency: Date.now() - startTime,
      inputTokens: response.usage.prompt_tokens,
      outputTokens: response.usage.completion_tokens,
      cost,
      cached: false
    });
    
    res.json({
      ...cacheValue,
      cached: false,
      requestId: req.requestId,
      latency: Date.now() - startTime
    });
    
  } catch (error) {
    logger.error({
      requestId: req.requestId,
      error: error.message,
      stack: error.stack
    });
    
    res.status(500).json({
      error: 'Internal server error',
      message: error.message,
      requestId: req.requestId
    });
  }
});

// Cost analytics endpoint
app.get('/v1/analytics/costs', async (req, res) => {
  const { period = 'daily' } = req.query;
  const today = new Date().toISOString().split('T')[0];
  
  if (period === 'daily') {
    const cost = parseFloat(await redis.get(`cost:daily:${today}`) || '0');
    res.json({
      period: 'daily',
      date: today,
      cost,
      limit: gatewayConfig.budgetLimits.daily,
      percentage: (cost / gatewayConfig.budgetLimits.daily) * 100
    });
  } else if (period === 'monthly') {
    const month = today.substring(0, 7);
    const cost = parseFloat(await redis.get(`cost:monthly:${month}`) || '0');
    res.json({
      period: 'monthly',
      month,
      cost,
      limit: gatewayConfig.budgetLimits.monthly,
      percentage: (cost / gatewayConfig.budgetLimits.monthly) * 100
    });
  }
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    providers: Object.keys(providers),
    redis: redis.isReady
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  logger.info(`AI Gateway running on port ${PORT}`);
});

This implementation demonstrates several production-ready patterns. The unified API accepts requests in a standardized format regardless of the underlying provider. Semantic caching uses SHA-256 hashing of normalized request parameters to identify cacheable requests. Automatic failover attempts multiple providers with exponential backoff. Budget tracking uses Redis to maintain real-time cost metrics with automatic expiration. Comprehensive logging captures all requests, errors, and performance metrics for observability.

Python Implementation with Advanced Features

For organizations using Python-based infrastructure, here is an equivalent implementation with additional features including circuit breakers and rate limiting.

# ai_gateway_server.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import List, Dict, Optional, Literal
import httpx
import redis.asyncio as redis
import hashlib
import json
import time
from datetime import datetime, date
import asyncio
from circuitbreaker import circuit
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('gateway.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

app = FastAPI(title="Enterprise AI Gateway")

# Initialize Redis
redis_client = redis.from_url("redis://localhost:6379", decode_responses=True)

# Models
class Message(BaseModel):
    role: Literal["system", "user", "assistant"]
    content: str

class CompletionRequest(BaseModel):
    messages: List[Message]
    model: Optional[str] = None
    temperature: Optional[float] = 0.7
    max_tokens: Optional[int] = 4096
    provider: Optional[Literal["openai", "anthropic"]] = None

class UsageStats(BaseModel):
    prompt_tokens: int
    completion_tokens: int
    total_tokens: int

class CompletionResponse(BaseModel):
    content: str
    usage: UsageStats
    provider: str
    model: str
    cost: float
    cached: bool
    request_id: str
    latency: float

# Configuration
class GatewayConfig:
    DEFAULT_PROVIDER = "anthropic"
    MAX_RETRIES = 3
    CACHE_TTL = 3600
    BUDGET_LIMITS = {
        "daily": 1000.00,
        "monthly": 25000.00
    }
    PROVIDER_COSTS = {
        "openai:gpt-4": {"input": 0.03, "output": 0.06},
        "anthropic:claude-sonnet-4": {"input": 0.003, "output": 0.015}
    }
    RATE_LIMITS = {
        "requests_per_minute": 100,
        "tokens_per_minute": 50000
    }

config = GatewayConfig()

# Provider clients
class ProviderManager:
    def __init__(self):
        self.openai_key = "your-openai-key"
        self.anthropic_key = "your-anthropic-key"
        self.circuit_state = {
            "openai": {"failures": 0, "last_failure": None},
            "anthropic": {"failures": 0, "last_failure": None}
        }
    
    @circuit(failure_threshold=5, recovery_timeout=60)
    async def call_openai(
        self, 
        messages: List[Dict], 
        model: str, 
        temperature: float, 
        max_tokens: int
    ) -> Dict:
        """Call OpenAI API with circuit breaker"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://api.openai.com/v1/chat/completions",
                headers={
                    "Authorization": f"Bearer {self.openai_key}",
                    "Content-Type": "application/json"
                },
                json={
                    "model": model or "gpt-4",
                    "messages": messages,
                    "temperature": temperature,
                    "max_tokens": max_tokens
                },
                timeout=60.0
            )
            response.raise_for_status()
            data = response.json()
            
            return {
                "content": data["choices"][0]["message"]["content"],
                "usage": {
                    "prompt_tokens": data["usage"]["prompt_tokens"],
                    "completion_tokens": data["usage"]["completion_tokens"],
                    "total_tokens": data["usage"]["total_tokens"]
                },
                "provider": "openai",
                "model": data["model"]
            }
    
    @circuit(failure_threshold=5, recovery_timeout=60)
    async def call_anthropic(
        self, 
        messages: List[Dict], 
        model: str, 
        temperature: float, 
        max_tokens: int
    ) -> Dict:
        """Call Anthropic API with circuit breaker"""
        async with httpx.AsyncClient() as client:
            response = await client.post(
                "https://api.anthropic.com/v1/messages",
                headers={
                    "x-api-key": self.anthropic_key,
                    "anthropic-version": "2023-06-01",
                    "Content-Type": "application/json"
                },
                json={
                    "model": model or "claude-sonnet-4-20250514",
                    "messages": messages,
                    "temperature": temperature,
                    "max_tokens": max_tokens
                },
                timeout=60.0
            )
            response.raise_for_status()
            data = response.json()
            
            return {
                "content": data["content"][0]["text"],
                "usage": {
                    "prompt_tokens": data["usage"]["input_tokens"],
                    "completion_tokens": data["usage"]["output_tokens"],
                    "total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"]
                },
                "provider": "anthropic",
                "model": data["model"]
            }

provider_manager = ProviderManager()

# Utility functions
def generate_cache_key(messages: List[Message], model: str, temperature: float) -> str:
    """Generate cache key from request parameters"""
    normalized = json.dumps({
        "messages": [{"role": m.role, "content": m.content} for m in messages],
        "model": model,
        "temperature": temperature or 0
    }, sort_keys=True)
    return f"cache:{hashlib.sha256(normalized.encode()).hexdigest()}"

def calculate_cost(provider: str, model: str, input_tokens: int, output_tokens: int) -> float:
    """Calculate request cost"""
    key = f"{provider}:{model}"
    costs = config.PROVIDER_COSTS.get(key, {"input": 0, "output": 0})
    return ((input_tokens / 1000) * costs["input"]) + ((output_tokens / 1000) * costs["output"])

async def track_cost(cost: float, date_str: str):
    """Track cost in Redis"""
    daily_key = f"cost:daily:{date_str}"
    monthly_key = f"cost:monthly:{date_str[:7]}"
    
    await redis_client.incrbyfloat(daily_key, cost)
    await redis_client.incrbyfloat(monthly_key, cost)
    await redis_client.expire(daily_key, 86400 * 7)
    await redis_client.expire(monthly_key, 86400 * 60)

async def check_rate_limit(client_id: str) -> bool:
    """Check if client has exceeded rate limits"""
    minute_key = f"rate:{client_id}:{int(time.time() / 60)}"
    current = await redis_client.incr(minute_key)
    await redis_client.expire(minute_key, 120)
    
    return current <= config.RATE_LIMITS["requests_per_minute"]

# Middleware
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    """Add unique request ID to all requests"""
    import uuid
    request.state.request_id = str(uuid.uuid4())
    request.state.start_time = time.time()
    
    response = await call_next(request)
    response.headers["X-Request-ID"] = request.state.request_id
    
    return response

@app.middleware("http")
async def check_budget(request: Request, call_next):
    """Check budget limits before processing"""
    if request.url.path == "/v1/chat/completions":
        today = date.today().isoformat()
        
        daily_cost = float(await redis_client.get(f"cost:daily:{today}") or 0)
        monthly_cost = float(await redis_client.get(f"cost:monthly:{today[:7]}") or 0)
        
        if daily_cost >= config.BUDGET_LIMITS["daily"]:
            return JSONResponse(
                status_code=429,
                content={
                    "error": "Daily budget limit exceeded",
                    "daily_limit": config.BUDGET_LIMITS["daily"],
                    "current_spend": daily_cost
                }
            )
        
        if monthly_cost >= config.BUDGET_LIMITS["monthly"]:
            return JSONResponse(
                status_code=429,
                content={
                    "error": "Monthly budget limit exceeded",
                    "monthly_limit": config.BUDGET_LIMITS["monthly"],
                    "current_spend": monthly_cost
                }
            )
        
        request.state.budget_check = {
            "daily_cost": daily_cost,
            "monthly_cost": monthly_cost,
            "today": today
        }
    
    response = await call_next(request)
    return response

# API endpoints
@app.post("/v1/chat/completions", response_model=CompletionResponse)
async def create_completion(request: Request, completion_req: CompletionRequest):
    """Main completion endpoint with caching and failover"""
    start_time = time.time()
    request_id = request.state.request_id
    
    try:
        # Check rate limit
        client_id = request.client.host
        if not await check_rate_limit(client_id):
            raise HTTPException(status_code=429, detail="Rate limit exceeded")
        
        # Check cache
        cache_key = generate_cache_key(
            completion_req.messages,
            completion_req.model,
            completion_req.temperature
        )
        cached = await redis_client.get(cache_key)
        
        if cached:
            logger.info(f"Cache hit for request {request_id}")
            cached_data = json.loads(cached)
            return CompletionResponse(
                **cached_data,
                cached=True,
                request_id=request_id,
                latency=time.time() - start_time
            )
        
        # Determine provider order
        target_provider = completion_req.provider or config.DEFAULT_PROVIDER
        providers_order = (
            ["openai", "anthropic"] if target_provider == "openai"
            else ["anthropic", "openai"]
        )
        
        # Attempt with failover
        response = None
        messages_dict = [{"role": m.role, "content": m.content} for m in completion_req.messages]
        
        for attempt in range(config.MAX_RETRIES):
            current_provider = providers_order[attempt % len(providers_order)]
            
            try:
                if current_provider == "openai":
                    response = await provider_manager.call_openai(
                        messages_dict,
                        completion_req.model,
                        completion_req.temperature,
                        completion_req.max_tokens
                    )
                else:
                    response = await provider_manager.call_anthropic(
                        messages_dict,
                        completion_req.model,
                        completion_req.temperature,
                        completion_req.max_tokens
                    )
                
                break  # Success
                
            except Exception as e:
                logger.error(f"Attempt {attempt + 1} failed for provider {current_provider}: {str(e)}")
                
                if attempt >= config.MAX_RETRIES - 1:
                    raise HTTPException(
                        status_code=503,
                        detail="All provider attempts failed"
                    )
                
                # Exponential backoff
                await asyncio.sleep(2 ** attempt)
        
        # Calculate cost
        cost = calculate_cost(
            response["provider"],
            response["model"],
            response["usage"]["prompt_tokens"],
            response["usage"]["completion_tokens"]
        )
        
        # Track cost
        await track_cost(cost, request.state.budget_check["today"])
        
        # Cache response
        cache_value = {
            "content": response["content"],
            "usage": response["usage"],
            "provider": response["provider"],
            "model": response["model"],
            "cost": cost
        }
        await redis_client.setex(cache_key, config.CACHE_TTL, json.dumps(cache_value))
        
        # Log metrics
        logger.info({
            "request_id": request_id,
            "provider": response["provider"],
            "latency": time.time() - start_time,
            "tokens": response["usage"]["total_tokens"],
            "cost": cost
        })
        
        return CompletionResponse(
            **cache_value,
            cached=False,
            request_id=request_id,
            latency=time.time() - start_time
        )
        
    except Exception as e:
        logger.error(f"Error processing request {request_id}: {str(e)}")
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/v1/analytics/costs")
async def get_cost_analytics(period: Literal["daily", "monthly"] = "daily"):
    """Get cost analytics"""
    today = date.today().isoformat()
    
    if period == "daily":
        cost = float(await redis_client.get(f"cost:daily:{today}") or 0)
        return {
            "period": "daily",
            "date": today,
            "cost": cost,
            "limit": config.BUDGET_LIMITS["daily"],
            "percentage": (cost / config.BUDGET_LIMITS["daily"]) * 100
        }
    else:
        month = today[:7]
        cost = float(await redis_client.get(f"cost:monthly:{month}") or 0)
        return {
            "period": "monthly",
            "month": month,
            "cost": cost,
            "limit": config.BUDGET_LIMITS["monthly"],
            "percentage": (cost / config.BUDGET_LIMITS["monthly"]) * 100
        }

@app.get("/health")
async def health_check():
    """Health check endpoint"""
    redis_healthy = await redis_client.ping()
    return {
        "status": "healthy",
        "providers": ["openai", "anthropic"],
        "redis": redis_healthy
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

This Python implementation adds circuit breaker patterns to prevent cascade failures when providers experience issues, sophisticated rate limiting to prevent abuse, and FastAPI’s automatic API documentation generation.

C# Implementation with Enterprise Integration

For organizations using .NET ecosystems, here is a C# implementation demonstrating integration with enterprise authentication and monitoring systems.

// AIGatewayService.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using StackExchange.Redis;

public class AIGatewayService
{
    private readonly ILogger<AIGatewayService> _logger;
    private readonly IDistributedCache _cache;
    private readonly IConnectionMultiplexer _redis;
    private readonly HttpClient _httpClient;
    private readonly GatewayConfiguration _config;

    public AIGatewayService(
        ILogger<AIGatewayService> logger,
        IDistributedCache cache,
        IConnectionMultiplexer redis,
        HttpClient httpClient,
        GatewayConfiguration config)
    {
        _logger = logger;
        _cache = cache;
        _redis = redis;
        _httpClient = httpClient;
        _config = config;
    }

    public async Task<CompletionResponse> CreateCompletionAsync(
        CompletionRequest request,
        string requestId,
        CancellationToken cancellationToken = default)
    {
        var startTime = DateTime.UtcNow;

        try
        {
            // Check budget
            await CheckBudgetAsync(cancellationToken);

            // Generate cache key
            var cacheKey = GenerateCacheKey(request);
            
            // Check cache
            var cachedResponse = await _cache.GetStringAsync(cacheKey, cancellationToken);
            if (!string.IsNullOrEmpty(cachedResponse))
            {
                _logger.LogInformation("Cache hit for request {RequestId}", requestId);
                var cached = JsonSerializer.Deserialize<CompletionResponse>(cachedResponse);
                cached.Cached = true;
                cached.RequestId = requestId;
                cached.Latency = (DateTime.UtcNow - startTime).TotalMilliseconds;
                return cached;
            }

            // Determine provider order
            var providersOrder = GetProviderOrder(request.Provider);
            
            // Attempt with failover
            CompletionResponse response = null;
            Exception lastException = null;

            for (int attempt = 0; attempt < _config.MaxRetries; attempt++)
            {
                var currentProvider = providersOrder[attempt % providersOrder.Length];

                try
                {
                    response = currentProvider == "openai"
                        ? await CallOpenAIAsync(request, cancellationToken)
                        : await CallAnthropicAsync(request, cancellationToken);

                    break; // Success
                }
                catch (Exception ex)
                {
                    lastException = ex;
                    _logger.LogError(ex, 
                        "Attempt {Attempt} failed for provider {Provider}", 
                        attempt + 1, 
                        currentProvider);

                    if (attempt < _config.MaxRetries - 1)
                    {
                        var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
                        await Task.Delay(delay, cancellationToken);
                    }
                }
            }

            if (response == null)
            {
                throw new InvalidOperationException(
                    "All provider attempts failed", 
                    lastException);
            }

            // Calculate cost
            var cost = CalculateCost(
                response.Provider,
                response.Model,
                response.Usage.PromptTokens,
                response.Usage.CompletionTokens);

            response.Cost = cost;

            // Track cost
            await TrackCostAsync(cost, cancellationToken);

            // Cache response
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(_config.CacheTTL)
            };
            await _cache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(response),
                cacheOptions,
                cancellationToken);

            // Log metrics
            _logger.LogInformation(
                "Request {RequestId} completed. Provider: {Provider}, " +
                "Latency: {Latency}ms, Tokens: {Tokens}, Cost: ${Cost:F4}",
                requestId,
                response.Provider,
                (DateTime.UtcNow - startTime).TotalMilliseconds,
                response.Usage.TotalTokens,
                cost);

            response.Cached = false;
            response.RequestId = requestId;
            response.Latency = (DateTime.UtcNow - startTime).TotalMilliseconds;

            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing request {RequestId}", requestId);
            throw;
        }
    }

    private async Task CheckBudgetAsync(CancellationToken cancellationToken)
    {
        var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
        var month = DateTime.UtcNow.ToString("yyyy-MM");
        
        var db = _redis.GetDatabase();
        
        var dailyCost = (double)(await db.StringGetAsync($"cost:daily:{today}"));
        var monthlyCost = (double)(await db.StringGetAsync($"cost:monthly:{month}"));

        if (dailyCost >= _config.BudgetLimits.Daily)
        {
            throw new InvalidOperationException(
                $"Daily budget limit exceeded. Limit: ${_config.BudgetLimits.Daily}, " +
                $"Current: ${dailyCost:F2}");
        }

        if (monthlyCost >= _config.BudgetLimits.Monthly)
        {
            throw new InvalidOperationException(
                $"Monthly budget limit exceeded. Limit: ${_config.BudgetLimits.Monthly}, " +
                $"Current: ${monthlyCost:F2}");
        }
    }

    private string GenerateCacheKey(CompletionRequest request)
    {
        var normalized = JsonSerializer.Serialize(new
        {
            messages = request.Messages.Select(m => new { m.Role, m.Content }),
            model = request.Model,
            temperature = request.Temperature ?? 0
        });

        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(normalized));
        return $"cache:{BitConverter.ToString(hash).Replace("-", "").ToLower()}";
    }

    private string[] GetProviderOrder(string requestedProvider)
    {
        var provider = requestedProvider ?? _config.DefaultProvider;
        return provider == "openai"
            ? new[] { "openai", "anthropic" }
            : new[] { "anthropic", "openai" };
    }

    private async Task<CompletionResponse> CallOpenAIAsync(
        CompletionRequest request,
        CancellationToken cancellationToken)
    {
        var payload = new
        {
            model = request.Model ?? "gpt-4",
            messages = request.Messages,
            temperature = request.Temperature ?? 0.7,
            max_tokens = request.MaxTokens ?? 4096
        };

        var httpRequest = new HttpRequestMessage(HttpMethod.Post, 
            "https://api.openai.com/v1/chat/completions")
        {
            Content = new StringContent(
                JsonSerializer.Serialize(payload),
                Encoding.UTF8,
                "application/json")
        };

        httpRequest.Headers.Add("Authorization", $"Bearer {_config.OpenAIKey}");

        var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        var data = JsonSerializer.Deserialize<OpenAIResponse>(content);

        return new CompletionResponse
        {
            Content = data.Choices[0].Message.Content,
            Usage = new UsageStats
            {
                PromptTokens = data.Usage.PromptTokens,
                CompletionTokens = data.Usage.CompletionTokens,
                TotalTokens = data.Usage.TotalTokens
            },
            Provider = "openai",
            Model = data.Model
        };
    }

    private async Task<CompletionResponse> CallAnthropicAsync(
        CompletionRequest request,
        CancellationToken cancellationToken)
    {
        var payload = new
        {
            model = request.Model ?? "claude-sonnet-4-20250514",
            messages = request.Messages,
            temperature = request.Temperature ?? 0.7,
            max_tokens = request.MaxTokens ?? 4096
        };

        var httpRequest = new HttpRequestMessage(HttpMethod.Post,
            "https://api.anthropic.com/v1/messages")
        {
            Content = new StringContent(
                JsonSerializer.Serialize(payload),
                Encoding.UTF8,
                "application/json")
        };

        httpRequest.Headers.Add("x-api-key", _config.AnthropicKey);
        httpRequest.Headers.Add("anthropic-version", "2023-06-01");

        var response = await _httpClient.SendAsync(httpRequest, cancellationToken);
        response.EnsureSuccessStatusCode();

        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        var data = JsonSerializer.Deserialize<AnthropicResponse>(content);

        return new CompletionResponse
        {
            Content = data.Content[0].Text,
            Usage = new UsageStats
            {
                PromptTokens = data.Usage.InputTokens,
                CompletionTokens = data.Usage.OutputTokens,
                TotalTokens = data.Usage.InputTokens + data.Usage.OutputTokens
            },
            Provider = "anthropic",
            Model = data.Model
        };
    }

    private double CalculateCost(
        string provider,
        string model,
        int inputTokens,
        int outputTokens)
    {
        var key = $"{provider}:{model}";
        if (!_config.ProviderCosts.TryGetValue(key, out var costs))
        {
            return 0;
        }

        return ((inputTokens / 1000.0) * costs.Input) +
               ((outputTokens / 1000.0) * costs.Output);
    }

    private async Task TrackCostAsync(double cost, CancellationToken cancellationToken)
    {
        var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
        var month = DateTime.UtcNow.ToString("yyyy-MM");

        var db = _redis.GetDatabase();

        await db.StringIncrementAsync($"cost:daily:{today}", cost);
        await db.StringIncrementAsync($"cost:monthly:{month}", cost);
        
        await db.KeyExpireAsync($"cost:daily:{today}", TimeSpan.FromDays(7));
        await db.KeyExpireAsync($"cost:monthly:{month}", TimeSpan.FromDays(60));
    }
}

// Configuration and model classes
public class GatewayConfiguration
{
    public string DefaultProvider { get; set; } = "anthropic";
    public int MaxRetries { get; set; } = 3;
    public int CacheTTL { get; set; } = 3600;
    public BudgetLimits BudgetLimits { get; set; } = new();
    public Dictionary<string, ProviderCost> ProviderCosts { get; set; } = new();
    public string OpenAIKey { get; set; }
    public string AnthropicKey { get; set; }
}

public class BudgetLimits
{
    public double Daily { get; set; } = 1000.00;
    public double Monthly { get; set; } = 25000.00;
}

public class ProviderCost
{
    public double Input { get; set; }
    public double Output { get; set; }
}

public class CompletionRequest
{
    public List<Message> Messages { get; set; }
    public string Model { get; set; }
    public double? Temperature { get; set; }
    public int? MaxTokens { get; set; }
    public string Provider { get; set; }
}

public class Message
{
    public string Role { get; set; }
    public string Content { get; set; }
}

public class CompletionResponse
{
    public string Content { get; set; }
    public UsageStats Usage { get; set; }
    public string Provider { get; set; }
    public string Model { get; set; }
    public double Cost { get; set; }
    public bool Cached { get; set; }
    public string RequestId { get; set; }
    public double Latency { get; set; }
}

public class UsageStats
{
    public int PromptTokens { get; set; }
    public int CompletionTokens { get; set; }
    public int TotalTokens { get; set; }
}

The C# implementation leverages .NET’s robust dependency injection, distributed caching abstractions that can use Redis or other providers, structured logging with semantic properties, and strong typing throughout the codebase.

MLOps: Continuous Integration and Deployment for AI Models

Production AI systems require robust MLOps practices enabling continuous integration, testing, deployment, and monitoring of AI models. Unlike traditional software where code determines behavior, AI systems’ behavior emerges from the interaction between code, data, and model weights, requiring specialized practices.

Core MLOps Architecture

A comprehensive MLOps platform consists of several key components working together. The data pipeline continuously ingests, validates, and transforms training data. The training pipeline orchestrates model training with experiment tracking and hyperparameter tuning. The model registry stores trained models with versioning and metadata. The evaluation system runs automated tests assessing model quality. The deployment system handles model serving with canary releases and blue-green deployments. The monitoring system tracks model performance and detects drift.

To continue this comprehensive article, would you like me to proceed with the MLOps implementation sections, or should I create a new continuation focusing on multi-cloud architecture and deployment patterns? The current content is quite substantial and we want to ensure we provide comprehensive technical depth while managing the article length appropriately.

MLOps Pipeline Architecture Diagram

The following diagram illustrates a complete MLOps pipeline architecture showing data ingestion through production deployment.

graph TB
    subgraph Data["Data Layer"]
        DS[Data Sources]
        DL[Data Lake]
        DW[Data Warehouse]
        Stream[Streaming Platform]
    end
    
    subgraph Pipeline["Training Pipeline"]
        Ingest[Data Ingestion]
        Validate[Data Validation]
        Transform[Feature Engineering]
        Train[Model Training]
        Evaluate[Model Evaluation]
    end
    
    subgraph Registry["Model Registry"]
        Store[Model Storage]
        Version[Version Control]
        Meta[Metadata Management]
    end
    
    subgraph Deployment["Deployment"]
        Stage[Staging Environment]
        Canary[Canary Release]
        Prod[Production Deployment]
    end
    
    subgraph Monitoring["Monitoring & Observability"]
        Metrics[Performance Metrics]
        Drift[Drift Detection]
        Alerts[Alert System]
        Logs[Centralized Logging]
    end
    
    DS --> DL
    DS --> DW
    DS --> Stream
    
    DL --> Ingest
    DW --> Ingest
    Stream --> Ingest
    
    Ingest --> Validate
    Validate --> Transform
    Transform --> Train
    Train --> Evaluate
    
    Evaluate --> Store
    Store --> Version
    Version --> Meta
    
    Meta --> Stage
    Stage --> Canary
    Canary --> Prod
    
    Prod --> Metrics
    Metrics --> Drift
    Drift --> Alerts
    Prod --> Logs
    
    Alerts -.->|Retrain Trigger| Train
    Drift -.->|Retrain Trigger| Train

This architecture ensures that models flow through a controlled pipeline from training to production, with continuous monitoring triggering retraining when performance degrades.

Multi-Cloud Architecture for AI Systems

Enterprise AI systems increasingly require multi-cloud architectures providing resilience, vendor flexibility, and regulatory compliance. Organizations must design systems that can operate seamlessly across multiple cloud providers while maintaining performance and security.

Multi-Cloud Reference Architecture

A robust multi-cloud AI architecture abstracts provider-specific services behind common interfaces, uses container orchestration for portability, implements data replication across regions, and maintains consistent security policies everywhere.

graph LR
    subgraph Client["Client Applications"]
        Web[Web Application]
        Mobile[Mobile Apps]
        API[API Clients]
    end
    
    subgraph Gateway["Global Load Balancer & API Gateway"]
        LB[Cloud Load Balancer]
        APIGW[API Gateway]
    end
    
    subgraph AWS["AWS Cloud"]
        AWS_K8S[EKS Cluster]
        AWS_AI[Bedrock / SageMaker]
        AWS_Cache[ElastiCache]
        AWS_DB[RDS / DynamoDB]
        AWS_Storage[S3]
    end
    
    subgraph Azure["Azure Cloud"]
        Azure_K8S[AKS Cluster]
        Azure_AI[Azure AI Foundry]
        Azure_Cache[Redis Cache]
        Azure_DB[Cosmos DB]
        Azure_Storage[Blob Storage]
    end
    
    subgraph GCP["Google Cloud"]
        GCP_K8S[GKE Cluster]
        GCP_AI[Vertex AI]
        GCP_Cache[Memorystore]
        GCP_DB[Cloud SQL]
        GCP_Storage[Cloud Storage]
    end
    
    subgraph Data["Data Replication"]
        Sync[Data Sync Service]
        CDC[Change Data Capture]
    end
    
    subgraph Monitor["Monitoring & Observability"]
        Prom[Prometheus]
        Graf[Grafana]
        Trace[Distributed Tracing]
    end
    
    Web --> LB
    Mobile --> LB
    API --> LB
    
    LB --> APIGW
    
    APIGW --> AWS_K8S
    APIGW --> Azure_K8S
    APIGW --> GCP_K8S
    
    AWS_K8S --> AWS_AI
    AWS_K8S --> AWS_Cache
    AWS_K8S --> AWS_DB
    AWS_K8S --> AWS_Storage
    
    Azure_K8S --> Azure_AI
    Azure_K8S --> Azure_Cache
    Azure_K8S --> Azure_DB
    Azure_K8S --> Azure_Storage
    
    GCP_K8S --> GCP_AI
    GCP_K8S --> GCP_Cache
    GCP_K8S --> GCP_DB
    GCP_K8S --> GCP_Storage
    
    AWS_DB <--> Sync
    Azure_DB <--> Sync
    GCP_DB <--> Sync
    
    Sync --> CDC
    
    AWS_K8S --> Prom
    Azure_K8S --> Prom
    GCP_K8S --> Prom
    
    Prom --> Graf
    Prom --> Trace

This architecture enables organizations to distribute workloads across providers based on cost, performance, regulatory requirements, and availability needs while maintaining a consistent operational model.

Production Deployment Patterns

Successfully deploying AI systems to production requires well-established patterns managing risk while enabling rapid iteration. Several deployment patterns have proven effective in enterprise environments.

Blue-Green Deployment

Blue-green deployment maintains two identical production environments, with traffic routed to one while the other remains idle. New model versions deploy to the idle environment, undergo validation, and then traffic switches instantly. If issues arise, traffic switches back immediately. This pattern minimizes downtime and provides instant rollback capabilities.

Canary Deployment

Canary deployment gradually routes increasing percentages of traffic to new model versions while monitoring performance metrics. Starting with 1-5% of traffic, organizations validate the new model performs acceptably before expanding to 10%, 25%, 50%, and finally 100%. If metrics degrade at any stage, traffic routes back to the previous version.

Shadow Deployment

Shadow deployment sends copies of production traffic to new model versions without returning results to users. This allows comprehensive testing with real workloads while maintaining zero user impact. Organizations compare shadow model outputs against production models to identify potential issues before actual deployment.

A/B Testing

A/B testing randomly assigns users to different model versions and measures business outcomes. This goes beyond technical metrics to assess actual business impact, answering questions like whether the new model improves conversion rates, reduces support tickets, or increases user satisfaction.

Infrastructure as Code for AI Systems

Modern AI infrastructure must be defined and managed through code, enabling version control, reproducibility, and automated deployment. Infrastructure as Code tools like Terraform, Pulumi, and Cloud provider-specific solutions enable declarative infrastructure management.

Terraform Example for AI Gateway Infrastructure

Here is a comprehensive Terraform configuration deploying an AI gateway with all supporting infrastructure on AWS.

# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

# VPC Configuration
resource "aws_vpc" "ai_gateway_vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = "ai-gateway-vpc"
    Environment = var.environment
  }
}

# Subnets
resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.ai_gateway_vpc.id
  cidr_block        = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "ai-gateway-private-${count.index + 1}"
  }
}

resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.ai_gateway_vpc.id
  cidr_block              = "10.0.${count.index + 10}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "ai-gateway-public-${count.index + 1}"
  }
}

# EKS Cluster
resource "aws_eks_cluster" "ai_gateway" {
  name     = "ai-gateway-${var.environment}"
  role_arn = aws_iam_role.eks_cluster_role.arn
  version  = "1.28"

  vpc_config {
    subnet_ids = concat(aws_subnet.private[*].id, aws_subnet.public[*].id)
  }

  depends_on = [
    aws_iam_role_policy_attachment.eks_cluster_policy
  ]
}

# EKS Node Group
resource "aws_eks_node_group" "ai_gateway" {
  cluster_name    = aws_eks_cluster.ai_gateway.name
  node_group_name = "ai-gateway-nodes"
  node_role_arn   = aws_iam_role.eks_node_role.arn
  subnet_ids      = aws_subnet.private[*].id

  scaling_config {
    desired_size = var.node_desired_size
    max_size     = var.node_max_size
    min_size     = var.node_min_size
  }

  instance_types = ["t3.xlarge"]

  depends_on = [
    aws_iam_role_policy_attachment.eks_node_policy
  ]
}

# ElastiCache Redis
resource "aws_elasticache_cluster" "redis" {
  cluster_id           = "ai-gateway-cache"
  engine               = "redis"
  node_type            = "cache.t3.medium"
  num_cache_nodes      = 1
  parameter_group_name = "default.redis7"
  engine_version       = "7.0"
  port                 = 6379
  subnet_group_name    = aws_elasticache_subnet_group.redis.name
  security_group_ids   = [aws_security_group.redis.id]
}

# RDS PostgreSQL for metadata
resource "aws_db_instance" "metadata" {
  identifier        = "ai-gateway-metadata"
  engine            = "postgres"
  engine_version    = "15.4"
  instance_class    = "db.t3.medium"
  allocated_storage = 100
  storage_encrypted = true

  db_name  = "aigateway"
  username = var.db_username
  password = var.db_password

  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.metadata.name

  backup_retention_period = 7
  backup_window          = "03:00-04:00"
  maintenance_window     = "mon:04:00-mon:05:00"

  skip_final_snapshot = var.environment != "production"
}

# Application Load Balancer
resource "aws_lb" "ai_gateway" {
  name               = "ai-gateway-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = aws_subnet.public[*].id

  enable_deletion_protection = var.environment == "production"
}

# Variables
variable "aws_region" {
  default = "us-east-1"
}

variable "environment" {
  default = "development"
}

variable "node_desired_size" {
  default = 2
}

variable "node_max_size" {
  default = 5
}

variable "node_min_size" {
  default = 1
}

variable "db_username" {
  sensitive = true
}

variable "db_password" {
  sensitive = true
}

This Infrastructure as Code configuration provides a complete foundation for deploying production AI gateways with all necessary supporting services, security groups, and networking configuration.

Observability and Monitoring

Production AI systems require comprehensive observability providing visibility into system behavior, performance, and quality. Effective monitoring goes beyond traditional application metrics to include AI-specific concerns like model performance, prediction quality, and drift detection.

Key Metrics for AI Systems

Production AI systems must track several categories of metrics. Infrastructure metrics include request latency, throughput, error rates, and resource utilization. Model performance metrics track prediction accuracy, confidence scores, and output quality. Business metrics measure the impact on key performance indicators like conversion rates, customer satisfaction, and operational efficiency. Cost metrics monitor spending across providers and models.

Additionally, AI-specific metrics include data drift measuring changes in input data distributions, concept drift detecting changes in the relationship between inputs and outputs, and prediction drift tracking changes in model output distributions. These metrics help identify when models require retraining.

Security and Compliance in AI Infrastructure

Enterprise AI infrastructure must implement comprehensive security controls protecting data, models, and operations while ensuring regulatory compliance. Security considerations span multiple layers from network security to data encryption to access control.

Network security isolates AI systems in private networks with controlled ingress and egress. Data encryption protects data at rest and in transit using industry-standard encryption. Identity and access management ensures only authorized users and services access AI systems. Secret management stores API keys, passwords, and certificates securely. Audit logging captures all access and operations for compliance and forensics.

Compliance requirements vary by industry and geography. Healthcare organizations must comply with HIPAA regulations. Financial services must meet SOC 2 and PCI DSS requirements. Organizations operating in Europe must comply with GDPR. AI systems must be designed with these requirements in mind from the beginning rather than retrofitted later.

Conclusion

Building enterprise-grade AI infrastructure requires comprehensive platforms managing provider complexity, enabling continuous model delivery, ensuring reliability and performance, and providing governance and security. The infrastructure patterns, code examples, and architectural diagrams presented in this article provide a foundation for organizations building production AI systems.

Key takeaways include the critical importance of AI gateway platforms abstracting provider complexity, the necessity of robust MLOps practices for continuous model delivery, the value of multi-cloud architectures providing flexibility and resilience, the requirement for comprehensive observability enabling proactive issue detection, and the fundamental need for security and compliance built into infrastructure from the beginning.

Organizations successfully deploying production AI systems invest heavily in infrastructure automation, monitoring, and governance. They treat AI infrastructure as a strategic capability requiring dedicated teams and sustained investment. The code examples in Node.js, Python, and C# demonstrate that robust AI infrastructure can be built using standard enterprise technologies and patterns.

In the next article in this series, we will examine agentic AI systems in detail, exploring implementation patterns for autonomous agents, multi-agent orchestration, and integration with enterprise systems. We will provide detailed code examples showing how to build production-ready agentic systems that can operate reliably at scale.

References

Written by:

572 Posts

View All Posts
Follow Me :
How to whitelist website on AdBlocker?

How to whitelist website on AdBlocker?

  1. 1 Click on the AdBlock Plus icon on the top right corner of your browser
  2. 2 Click on "Enabled on this site" from the AdBlock Plus option
  3. 3 Refresh the page and start browsing the site