system-design47 min read

Design an OpenAI-Style Enterprise LLM Platform — The Complete Guide | Ayodhyya

Design an OpenAI-Style Enterprise LLM Platform

The Complete Guide to building a production-grade, multi-tenant platform for large language model inference, fine-tuning, and retrieval-augmented generation at enterprise scale

Senior+ Guide 55+ min read 10,000+ words Ayodhyya

Table of Contents

  1. Introduction — Why Build Your Own LLM Platform?
  2. The LLM Platform Landscape in 2026
  3. Functional and Non-Functional Requirements
  4. Capacity Estimation and Back-of-Envelope Math
  5. Data Model and Storage Schema
  6. High-Level System Architecture
  7. API Design — REST and Streaming Endpoints
  8. Model Serving and Inference Engine
  9. Fine-Tuning Pipeline
  10. Prompt Management and Versioning
  11. Token Billing and Usage Tracking
  12. Safety and Content Moderation
  13. Retrieval-Augmented Generation (RAG)
  14. Embeddings and Vector Store
  15. Rate Limiting and Quotas
  16. Multi-Tenant Workspace Isolation
  17. Model Versioning and A/B Testing
  18. Knowledge Base Management
  19. Admin Dashboard and Observability
  20. Monitoring, Logging, and Alerting
  21. Enterprise SSO and Compliance
  22. Cost Estimation
  23. Testing the Platform
  24. Interview Q&A
  25. Conclusion

1. Introduction — Why Build Your Own LLM Platform?

Large language models have moved from research labs into the core of enterprise product strategies. Companies across finance, healthcare, legal, and technology sectors now rely on LLMs for code generation, document summarization, conversational agents, and knowledge retrieval. Yet the gap between calling an API and running a reliable, auditable, cost-controlled platform at enterprise scale is enormous.

An OpenAI-style enterprise LLM platform is not just a thin wrapper around a model endpoint. It is a full-stack system that encompasses model serving infrastructure, fine-tuning pipelines, retrieval-augmented generation, token-level billing, content safety, multi-tenant isolation, prompt versioning, and deep observability. Every component must handle failures gracefully, scale horizontally, and satisfy regulatory requirements such as SOC 2, HIPAA, and GDPR.

In this guide we design the entire platform from scratch. We begin with requirements gathering, move through capacity estimation, data modeling, and system architecture, then dive deep into each subsystem. Every section includes C# code samples, Mermaid diagrams, data tables, and production-grade patterns. By the end you will have a blueprint you can adapt for your own organization.

Who is this guide for? Senior engineers, staff engineers, and architects who are responsible for building or evaluating an internal LLM platform. We assume familiarity with distributed systems, C#/.NET, Kubernetes, and relational databases.

The rise of open-weight models like Llama 3, Mistral, and Qwen 2.5 has made self-hosting economically viable for many organizations. When you control the full stack you gain the ability to fine-tune on proprietary data, enforce data residency, optimize cost per token, and guarantee SLAs that third-party APIs cannot match. This guide shows you exactly how to build that stack.

We will model the platform after the capabilities offered by OpenAI, Anthropic, and Cohere, but we will design every component to be model-agnostic. You should be able to plug in any transformer-based model served by vLLM, TensorRT-LLM, TGI, or ONNX Runtime without changing the platform layer. The key insight is that the platform is a control plane, and the model inference is a data plane. Separating these two concerns cleanly is what makes the system maintainable and extensible.

Throughout the article we emphasize production hardening. We discuss how to handle GPU failures, how to implement graceful degradation when a model is overloaded, how to audit every prompt and completion for compliance, and how to meter usage accurately across thousands of tenants. These are the details that separate a demo from a platform that can be trusted with mission-critical workloads.

2. The LLM Platform Landscape in 2026

The LLM platform market has matured considerably. OpenAI offers the most feature-complete hosted platform with Assistants, fine-tuning, and GPTs. Anthropic focuses on safety and long-context windows. Google Cloud Vertex AI provides a full MLOps pipeline with model garden. On the self-hosted side, vLLM and TensorRT-LLM dominate the inference layer, while tools like LangChain, LlamaIndex, and Semantic Kernel provide orchestration.

PlatformHostingFine-TuningRAG Built-inMulti-TenantOpen Source
OpenAI PlatformManagedYesAssistants APIOrganization-levelNo
Anthropic ClaudeManagedLimitedNoWorkspaceNo
Azure OpenAIManagedYesAzure AI SearchFull RBACNo
Vertex AIManagedPipeline-basedVertex AI SearchProject-levelNo
Self-hosted (vLLM)Self-managedCustomCustomCustomYes
Self-hosted (TGI)Self-managedCustomCustomCustomYes

For enterprise use cases, the decision between managed and self-hosted often comes down to three factors: data sovereignty, cost at scale, and customization depth. If your data cannot leave your VPC, self-hosting is the only option. If you need to fine-tune models on millions of proprietary examples, self-hosting gives you full control over the training loop. And if you need to optimize inference for specific hardware — for example, NVIDIA H100 GPUs with FP8 quantization — you need to manage the serving stack yourself.

The architecture we design in this guide supports both managed and self-hosted inference backends. The platform sends inference requests to a backend adapter layer that abstracts the differences between OpenAI API, vLLM, TGI, and custom endpoints. This adapter pattern means you can start with managed APIs and migrate to self-hosted inference as your volume grows, without changing any of the platform's client-facing APIs.

Another important trend is the convergence of LLM platforms with traditional data platforms. Retrieval-augmented generation requires vector databases, full-text search engines, and document stores. Platforms like Weaviate, Pinecone, Milvus, and pgvector are becoming essential components of the LLM stack. We dedicate entire sections to embeddings, vector stores, and RAG pipelines in this guide.

The regulatory landscape is also shifting. The EU AI Act requires transparency about AI-generated content, audit trails for high-risk applications, and human oversight mechanisms. A well-designed enterprise platform builds these requirements into the architecture from day one rather than bolting them on later. We address compliance throughout the guide, with dedicated coverage in the SSO and compliance section.

3. Functional and Non-Functional Requirements

Functional Requirements

  • Chat Completions: Support multi-turn conversations with system prompts, user messages, and assistant responses. Support streaming via Server-Sent Events.
  • Text Completions: Legacy completion endpoint for single-prompt inference.
  • Embeddings: Generate vector embeddings for text inputs. Support batch embedding for large document sets.
  • Fine-Tuning: Upload training data, configure hyperparameters, monitor training progress, and deploy fine-tuned models.
  • RAG / Assistants: Attach knowledge bases to conversations. Support file upload, chunking, and retrieval during inference.
  • Model Management: List available models, view model metadata, manage model versions, and route requests to specific model instances.
  • Usage Metering: Track token usage per request, per user, and per organization. Generate usage reports and invoices.
  • Content Safety: Filter harmful, toxic, or policy-violating content in both inputs and outputs.
  • Admin Dashboard: Manage organizations, users, API keys, quotas, and billing. View analytics and system health.
  • Audit Logging: Record every API call, prompt, completion, and administrative action for compliance purposes.

Non-Functional Requirements

RequirementTarget
Availability99.95% uptime (less than 22 minutes downtime per month)
Latency (TTFT)First token under 500ms for standard models, under 200ms for small models
Throughput10,000 concurrent chat sessions, 1,000 requests per second
ScalabilityAuto-scale from 10 to 500 GPU nodes based on demand
Data RetentionPrompts stored for 30 days by default, configurable per tenant
SecuritySOC 2 Type II, HIPAA BAA available, GDPR-compliant data handling
Multi-TenancyComplete data isolation between organizations, including at the inference layer
Disaster RecoveryRPO of 1 hour, RTO of 4 hours for full platform recovery

These requirements drive every architectural decision in the rest of the guide. The latency target for time-to-first-token means we need streaming inference with efficient KV-cache management. The throughput target requires horizontal scaling of both the API gateway and the inference backends. The compliance requirements dictate that we maintain detailed audit logs and encrypt data at rest and in transit.

Important: Enterprise customers often negotiate custom SLAs. Design your platform to support per-tenant SLA tiers so you can offer bronze, silver, and gold levels of service with different latency and availability guarantees.

4. Capacity Estimation and Back-of-Envelope Math

Before designing the system we need to estimate the scale of resources required. These numbers will inform our infrastructure choices and help us size the system correctly.

Assumptions

  • 10,000 organizations on the platform
  • 100 average active users per organization = 1,000,000 total users
  • Each user sends 50 requests per day on average
  • Average prompt length: 500 tokens input, 800 tokens output
  • Total daily requests: 50 million
  • Total daily tokens: 25 billion input + 40 billion output = 65 billion tokens per day

Throughput

MetricValue
Requests per second (average)50M / 86,400 = ~580 RPS
Peak RPS (3x average)~1,740 RPS
Input tokens per second (average)~290,000 tokens/sec
Output tokens per second (average)~463,000 tokens/sec

GPU Sizing

A single NVIDIA H100 80GB GPU can serve a 70B parameter model with tensor parallelism across 4 GPUs, achieving approximately 2,000 output tokens per second at batch size 32. For a 7B parameter model on a single H100, throughput reaches approximately 15,000 output tokens per second.

Assuming a mix of model sizes where 60% of requests go to small models (7B), 30% to medium models (70B), and 10% to large models (405B):

  • Small model throughput needed: 0.6 * 463,000 = 277,800 output tokens/sec. With 15,000 tokens/sec per H100, we need approximately 19 H100 GPUs for small models.
  • Medium model throughput needed: 0.3 * 463,000 = 138,900 output tokens/sec. With 2,000 tokens/sec per 4-GPU pod, we need approximately 280 GPUs (70 pods) for medium models.
  • Large model throughput needed: 0.1 * 463,000 = 46,300 output tokens/sec. With approximately 500 tokens/sec per 8-GPU pod for 405B, we need approximately 740 GPUs (93 pods) for large models.

Total GPU estimate: approximately 1,040 H100 GPUs for steady state, plus 50% headroom for peak traffic brings us to approximately 1,560 GPUs. At cloud pricing of $2.50 per GPU-hour this represents approximately $280,000 per month in compute costs alone.

Storage

ComponentDaily Volume30-Day Retention
Prompt/Completion Logs~325 GB~10 TB
Fine-Tuning Datasets~50 GB uploads~1.5 TB
Embeddings (Vector Store)~100 GB new vectors~3 TB
Audit Logs~20 GB~600 GB
Knowledge Base Documents~30 GB uploads~900 GB

These estimates show we need approximately 16 TB of hot storage and significantly more for long-term archival. Object storage like S3 or Azure Blob is ideal for the bulk of this data, with PostgreSQL and Redis handling the relational and caching layers.

5. Data Model and Storage Schema

The data model is the foundation of the platform. We use PostgreSQL as the primary relational store, with tables designed for multi-tenant isolation, efficient querying, and audit compliance.

Core Entities

SQL
CREATE TABLE organizations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name            VARCHAR(255) NOT NULL,
    plan            VARCHAR(50) NOT NULL DEFAULT 'free',
    monthly_token_quota BIGINT NOT NULL DEFAULT 1000000000,
    tokens_used     BIGINT NOT NULL DEFAULT 0,
    settings        JSONB NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE users (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    email           VARCHAR(255) NOT NULL UNIQUE,
    name            VARCHAR(255),
    role            VARCHAR(50) NOT NULL DEFAULT 'member',
    sso_provider    VARCHAR(100),
    sso_subject     VARCHAR(255),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE api_keys (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    user_id         UUID NOT NULL REFERENCES users(id),
    key_hash        VARCHAR(255) NOT NULL UNIQUE,
    key_prefix      VARCHAR(10) NOT NULL,
    name            VARCHAR(255),
    scopes          TEXT[] NOT NULL DEFAULT ARRAY['completions'],
    rate_limit_rps  INT NOT NULL DEFAULT 10,
    monthly_token_quota BIGINT,
    expires_at      TIMESTAMPTZ,
    last_used_at    TIMESTAMPTZ,
    is_active       BOOLEAN NOT NULL DEFAULT true,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE models (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    slug            VARCHAR(100) NOT NULL UNIQUE,
    display_name    VARCHAR(255) NOT NULL,
    provider        VARCHAR(100) NOT NULL,
    model_type      VARCHAR(50) NOT NULL DEFAULT 'chat',
    parameter_count BIGINT,
    context_window  INT NOT NULL DEFAULT 4096,
    pricing_input   DECIMAL(12,8) NOT NULL,
    pricing_output  DECIMAL(12,8) NOT NULL,
    is_active       BOOLEAN NOT NULL DEFAULT true,
    metadata        JSONB NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE completions (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    user_id         UUID REFERENCES users(id),
    api_key_id      UUID REFERENCES api_keys(id),
    model_slug      VARCHAR(100) NOT NULL,
    request_payload JSONB NOT NULL,
    response_payload JSONB,
    input_tokens    INT NOT NULL DEFAULT 0,
    output_tokens   INT NOT NULL DEFAULT 0,
    latency_ms      INT,
    first_token_ms  INT,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
    finish_reason   VARCHAR(50),
    safety_flags    JSONB DEFAULT '[]',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at    TIMESTAMPTZ
);

CREATE TABLE fine_tuning_jobs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    model_slug      VARCHAR(100) NOT NULL,
    base_model      VARCHAR(100) NOT NULL,
    training_file_id UUID NOT NULL,
    status          VARCHAR(50) NOT NULL DEFAULT 'pending',
    hyperparameters JSONB NOT NULL DEFAULT '{}',
    training_loss   DECIMAL(10,6),
    output_model_id UUID,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    completed_at    TIMESTAMPTZ
);

CREATE TABLE prompt_templates (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    name            VARCHAR(255) NOT NULL,
    version         INT NOT NULL DEFAULT 1,
    system_prompt   TEXT NOT NULL,
    parameters      JSONB NOT NULL DEFAULT '[]',
    model_slug      VARCHAR(100),
    is_active       BOOLEAN NOT NULL DEFAULT true,
    created_by      UUID REFERENCES users(id),
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(org_id, name, version)
);

CREATE TABLE usage_records (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    user_id         UUID REFERENCES users(id),
    api_key_id      UUID REFERENCES api_keys(id),
    model_slug      VARCHAR(100) NOT NULL,
    request_type    VARCHAR(50) NOT NULL,
    input_tokens    INT NOT NULL DEFAULT 0,
    output_tokens   INT NOT NULL DEFAULT 0,
    cost_cents      DECIMAL(10,6) NOT NULL DEFAULT 0,
    recorded_at     TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE knowledge_bases (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    org_id          UUID NOT NULL REFERENCES organizations(id),
    name            VARCHAR(255) NOT NULL,
    description     TEXT,
    embedding_model VARCHAR(100) NOT NULL DEFAULT 'text-embedding-3-small',
    chunk_size      INT NOT NULL DEFAULT 512,
    chunk_overlap   INT NOT NULL DEFAULT 50,
    document_count  INT NOT NULL DEFAULT 0,
    total_chunks    INT NOT NULL DEFAULT 0,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE kb_documents (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    kb_id           UUID NOT NULL REFERENCES knowledge_bases(id),
    filename        VARCHAR(500) NOT NULL,
    mime_type       VARCHAR(100),
    file_size_bytes BIGINT NOT NULL,
    chunk_count     INT NOT NULL DEFAULT 0,
    status          VARCHAR(50) NOT NULL DEFAULT 'pending',
    metadata        JSONB NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

Indexing Strategy

SQL
CREATE INDEX idx_completions_org_created ON completions(org_id, created_at DESC);
CREATE INDEX idx_completions_model ON completions(model_slug, created_at DESC);
CREATE INDEX idx_usage_org_recorded ON usage_records(org_id, recorded_at DESC);
CREATE INDEX idx_api_keys_hash ON api_keys(key_hash) WHERE is_active = true;
CREATE INDEX idx_users_org ON users(org_id);
CREATE INDEX idx_kb_docs_kb ON kb_documents(kb_id, status);

We use partitioning on the completions table by month to keep query performance high as the table grows to billions of rows. The usage_records table follows the same pattern. Object storage holds the actual prompt and completion payloads, while the database stores metadata and search fields.

6. High-Level System Architecture

graph TB Client["Client Applications"] Gateway["API Gateway
Rate Limiting, Auth, Routing"] Orch["Orchestration Service
.NET 8"] PromptSvc["Prompt Service"] RAGSvc["RAG Service"] SafetySvc["Safety Service"] BillingSvc["Billing Service"] ModelRouter["Model Router"] InferenceSmall["Inference: Small Models
vLLM + H100"] InferenceMed["Inference: Medium Models
vLLM + H100 x4"] InferenceLarge["Inference: Large Models
vLLM + H100 x8"] PG["PostgreSQL
Relational Data"] Redis["Redis Cluster
Cache, Rate Limits, Sessions"] VectorDB["Vector Store
pgvector / Weaviate"] ObjectStore["Object Storage
S3 / Azure Blob"] Kafka["Kafka / Event Hub
Usage Events, Audit Log"] Monitor["Observability Stack
Prometheus, Grafana, Jaeger"] AdminDash["Admin Dashboard
React SPA"] Client --> Gateway Gateway --> Orch Orch --> PromptSvc Orch --> RAGSvc Orch --> SafetySvc Orch --> BillingSvc Orch --> ModelRouter ModelRouter --> InferenceSmall ModelRouter --> InferenceMed ModelRouter --> InferenceLarge Orch --> PG Orch --> Redis RAGSvc --> VectorDB Orch --> ObjectStore BillingSvc --> Kafka SafetySvc --> Kafka Orch --> Monitor AdminDash --> Orch

Component Responsibilities

  • API Gateway: Handles TLS termination, JWT validation, rate limiting, and request routing. Implements circuit breakers for downstream services.
  • Orchestration Service: The central control plane. Receives validated requests, coordinates with all subsystems, assembles the final inference payload, and streams responses back to the client.
  • Prompt Service: Manages prompt templates, system prompt injection, few-shot example selection, and prompt versioning.
  • RAG Service: Receives user queries, generates embeddings, retrieves relevant document chunks, and injects them into the prompt context window.
  • Safety Service: Runs content classification on both inputs and outputs using a combination of keyword filters, toxicity classifiers, and policy-specific models.
  • Billing Service: Consumes usage events from Kafka, aggregates token counts, calculates costs, and updates organization balances. Enforces quota limits.
  • Model Router: Selects the appropriate inference backend based on model slug, availability, load, and tenant-specific routing rules.
  • Inference Backends: Stateful model serving clusters running vLLM or TensorRT-LLM with GPU acceleration. Each backend exposes an OpenAI-compatible API.

The architecture follows a microservices pattern with clear bounded contexts. The orchestration service is the only component that talks to clients directly. All internal communication happens over gRPC for synchronous calls and Kafka for asynchronous events. This separation allows us to scale, deploy, and fail each service independently.

Key design principle: The inference backends are stateless from the platform's perspective. They do not store billing data, user information, or audit logs. If a backend fails, the model router simply reroutes requests to another instance. This makes the data plane resilient and the control plane independently scalable.

7. API Design — REST and Streaming Endpoints

The API surface mirrors OpenAI's API design to reduce friction for developers who are already familiar with it. All endpoints accept and return JSON. Streaming endpoints use Server-Sent Events (SSE).

Core Endpoints

MethodEndpointDescription
POST/v1/chat/completionsMulti-turn chat with streaming support
POST/v1/completionsSingle-prompt text completion
POST/v1/embeddingsGenerate text embeddings
GET/v1/modelsList available models
GET/v1/models/{id}Get model details
POST/v1/fine-tuning/jobsCreate a fine-tuning job
GET/v1/fine-tuning/jobs/{id}Get fine-tuning job status
POST/v1/knowledge-basesCreate a knowledge base
POST/v1/knowledge-bases/{id}/documentsUpload document to knowledge base
GET/v1/usageQuery usage records

C# API Controller

C#
[ApiController]
[Route("v1")]
[Authorize]
public class ChatCompletionsController : ControllerBase
{
    private readonly IOrchestrationService _orchestration;
    private readonly IBillingService _billing;
    private readonly ISafetyService _safety;
    private readonly ILogger<ChatCompletionsController> _logger;

    public ChatCompletionsController(
        IOrchestrationService orchestration,
        IBillingService billing,
        ISafetyService safety,
        ILogger<ChatCompletionsController> logger)
    {
        _orchestration = orchestration;
        _billing = billing;
        _safety = safety;
        _logger = logger;
    }

    [HttpPost("chat/completions")]
    [ProducesResponseType(typeof(ChatCompletionResponse), 200)]
    public async Task<IActionResult> CreateChatCompletion(
        [FromBody] ChatCompletionRequest request,
        CancellationToken ct)
    {
        var orgId = HttpContext.GetOrganizationId();
        var userId = HttpContext.GetUserId();

        var quotaCheck = await _billing.CheckQuotaAsync(orgId, request.Model, ct);
        if (!quotaCheck.HasCapacity)
            return StatusCode(429, new { error = "Monthly token quota exceeded" });

        var safetyResult = await _safety.CheckInputAsync(
            request.Messages, orgId, ct);
        if (safetyResult.IsBlocked)
            return BadRequest(new { error = safetyResult.BlockReason });

        if (request.Stream)
            return await StreamCompletion(request, orgId, userId, ct);

        var response = await _orchestration.CompleteAsync(request, orgId, userId, ct);

        await _billing.RecordUsageAsync(new UsageRecord
        {
            OrgId = orgId,
            UserId = userId,
            ModelSlug = request.Model,
            RequestType = "chat",
            InputTokens = response.Usage.PromptTokens,
            OutputTokens = response.Usage.CompletionTokens,
            CostCents = CalculateCost(request.Model, response.Usage)
        }, ct);

        var outputSafety = await _safety.CheckOutputAsync(
            response.Choices, orgId, ct);
        if (outputSafety.IsBlocked)
            response.Choices = new List<Choice>();

        return Ok(response);
    }

    private async Task<IActionResult> StreamCompletion(
        ChatCompletionRequest request,
        Guid orgId,
        Guid userId,
        CancellationToken ct)
    {
        Response.Headers.ContentType = "text/event-stream";
        Response.Headers.CacheControl = "no-cache";

        var stream = _orchestration.StreamCompleteAsync(request, orgId, userId, ct);
        var totalInput = 0;
        var totalOutput = 0;

        await foreach (var chunk in stream.WithCancellation(ct))
        {
            if (chunk.Usage != null)
            {
                totalInput = chunk.Usage.PromptTokens;
                totalOutput = chunk.Usage.CompletionTokens;
            }

            var json = JsonSerializer.Serialize(chunk);
            await Response.WriteAsync($"data: {json}\n\n", ct);
            await Response.Body.FlushAsync(ct);
        }

        await _billing.RecordUsageAsync(new UsageRecord
        {
            OrgId = orgId,
            UserId = userId,
            ModelSlug = request.Model,
            RequestType = "chat_stream",
            InputTokens = totalInput,
            OutputTokens = totalOutput,
            CostCents = CalculateCost(request.Model,
                new TokenUsage { PromptTokens = totalInput,
                    CompletionTokens = totalOutput })
        }, ct);

        await Response.WriteAsync("data: [DONE]\n\n", ct);
        return Ok();
    }

    private static decimal CalculateCost(string model, TokenUsage usage)
    {
        var pricing = ModelPricing.Get(model);
        return usage.InputTokens * pricing.InputPerToken
             + usage.OutputTokens * pricing.OutputPerToken;
    }
}

Streaming Protocol

The streaming protocol uses Server-Sent Events. Each chunk contains a delta with the newly generated token. The final chunk includes a usage object with the total token counts. This allows clients to display tokens as they arrive while still getting accurate billing information at the end of the stream.

JSON
{
  "id": "cmpl-abc123",
  "object": "chat.completion.chunk",
  "created": 1720886400,
  "model": "gpt-4o-enterprise",
  "choices": [{
    "index": 0,
    "delta": { "content": "The capital of France" },
    "finish_reason": null
  }]
}
...
{
  "id": "cmpl-abc123",
  "object": "chat.completion.chunk",
  "created": 1720886401,
  "model": "gpt-4o-enterprise",
  "choices": [{
    "index": 0,
    "delta": {},
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 42,
    "completion_tokens": 156,
    "total_tokens": 198
  }
}

8. Model Serving and Inference Engine

The inference engine is the most resource-intensive part of the platform. It runs transformer models on GPU hardware and must maximize throughput while maintaining low latency. We use vLLM as the primary serving framework because of its PagedAttention mechanism, continuous batching, and OpenAI-compatible API.

vLLM Deployment Architecture

graph LR Router["Model Router"] vLLM1["vLLM Instance 1
Llama 3.1 8B
1x H100"] vLLM2["vLLM Instance 2
Llama 3.1 70B
4x H100"] vLLM3["vLLM Instance 3
Llama 3.1 405B
8x H100"] LB["Load Balancer"] Router --> LB LB --> vLLM1 LB --> vLLM2 LB --> vLLM3

vLLM Configuration

YAML
# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama-3-1-70b
  namespace: inference
spec:
  replicas: 4
  selector:
    matchLabels:
      model: llama-3.1-70b
  template:
    metadata:
      labels:
        model: llama-3.1-70b
    spec:
      containers:
      - name: vllm
        image: vllm/vllm-openai:latest
        args:
        - "--model"
        - "meta-llama/Llama-3.1-70B-Instruct"
        - "--tensor-parallel-size"
        - "4"
        - "--max-model-len"
        - "131072"
        - "--gpu-memory-utilization"
        - "0.92"
        - "--enable-prefix-caching"
        - "--max-num-batched-tokens"
        - "65536"
        - "--max-num-seqs"
        - "256"
        - "--enforce-eager"
        - "false"
        ports:
        - containerPort: 8000
        resources:
          limits:
            nvidia.com/gpu: 4
            memory: 320Gi
          requests:
            cpu: "16"
            memory: 128Gi
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 120
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 180
          periodSeconds: 30

GPU Memory Optimization

Managing GPU memory is critical for inference performance. Key techniques include:

  • KV-Cache Paging (PagedAttention): vLLM's PagedAttention eliminates memory fragmentation by managing the key-value cache in fixed-size blocks. This allows the server to serve more concurrent requests without running out of memory.
  • Prefix Caching: When multiple requests share the same system prompt, the KV-cache for the shared prefix is computed once and reused. This is particularly valuable in enterprise scenarios where many requests share a base prompt template.
  • Quantization: For cost-sensitive deployments, we support AWQ and GPTQ quantization which reduces model memory by 4x with minimal quality degradation. A 70B model that requires 140GB in FP16 can fit on a single H100 with AWQ 4-bit quantization.
  • Continuous Batching: Rather than waiting for a full batch to complete, vLLM adds new requests to the batch as soon as a slot opens up. This keeps GPU utilization high and reduces average latency.

C# Inference Client

C#
public class VLLMInferenceClient : IInferenceClient
{
    private readonly HttpClient _http;
    private readonly ILogger<VLLMInferenceClient> _logger;

    public VLLMInferenceClient(HttpClient http,
        ILogger<VLLMInferenceClient> logger)
    {
        _http = http;
        _logger = logger;
    }

    public async Task<InferenceResponse> CompleteAsync(
        InferenceRequest request,
        CancellationToken ct)
    {
        var payload = new
        {
            model = request.ModelSlug,
            messages = request.Messages.Select(m => new
            {
                role = m.Role,
                content = m.Content
            }),
            temperature = request.Temperature,
            max_tokens = request.MaxTokens,
            top_p = request.TopP,
            stop = request.StopSequences
        };

        var json = JsonSerializer.Serialize(payload);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        var response = await _http.PostAsync("/v1/chat/completions", content, ct);
        response.EnsureSuccessStatusCode();

        var body = await response.Content.ReadAsStringAsync(ct);
        return JsonSerializer.Deserialize<InferenceResponse>(body);
    }

    public async IAsyncEnumerable<InferenceChunk> StreamCompleteAsync(
        InferenceRequest request,
        [EnumeratorCancellation] CancellationToken ct)
    {
        var payload = new
        {
            model = request.ModelSlug,
            messages = request.Messages.Select(m => new
            {
                role = m.Role,
                content = m.Content
            }),
            temperature = request.Temperature,
            max_tokens = request.MaxTokens,
            stream = true
        };

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

        var response = await _http.SendAsync(httpRequest,
            HttpCompletionOption.ResponseHeadersRead, ct);
        response.EnsureSuccessStatusCode();

        using var stream = await response.Content.ReadAsStreamAsync(ct);
        using var reader = new StreamReader(stream);

        while (!reader.EndOfStream)
        {
            var line = await reader.ReadLineAsync(ct);
            if (string.IsNullOrEmpty(line)) continue;
            if (line.StartsWith("data: "))
            {
                var data = line[6..];
                if (data == "[DONE]") yield break;
                var chunk = JsonSerializer.Deserialize<InferenceChunk>(data);
                if (chunk != null) yield return chunk;
            }
        }
    }
}

9. Fine-Tuning Pipeline

Fine-tuning allows enterprise customers to adapt base models to their specific domains. The pipeline handles dataset upload, validation, training job orchestration, checkpoint management, and model deployment.

Pipeline Stages

graph TB Upload["Dataset Upload
(S3)"] Validate["Validation
Format, Quality, PII Check"] Queue["Training Queue
Priority-based Scheduling"] Train["Training Job
PyTorch + DeepSpeed"] Evaluate["Evaluation
Held-out Metrics"] Register["Model Registry
Version & Metadata"] Deploy["Deployment
Canary → Production"] Upload --> Validate Validate --> Queue Queue --> Train Train --> Evaluate Evaluate --> Register Register --> Deploy

Fine-Tuning Service

C#
public class FineTuningService : IFineTuningService
{
    private readonly IPromptRepository _prompts;
    private readonly ITrainingJobQueue _jobQueue;
    private readonly IModelRegistry _registry;
    private readonly IBlobStorage _storage;

    public async Task<FineTuningJob> CreateJobAsync(
        CreateFineTuningRequest request,
        Guid orgId,
        CancellationToken ct)
    {
        var dataset = await _storage.DownloadAsync(
            request.TrainingFileId, ct);

        var validation = ValidateDataset(dataset);
        if (!validation.IsValid)
            throw new ValidationException(
                $"Dataset validation failed: {validation.ErrorMessages}");

        if (validation.ContainsPii)
            throw new ValidationException(
                "Dataset contains PII. Please redact personal information.");

        var job = new FineTuningJob
        {
            Id = Guid.NewGuid(),
            OrgId = orgId,
            BaseModel = request.BaseModel,
            Status = "queued",
            Hyperparameters = new Hyperparameters
            {
                Epochs = request.Epochs ?? 3,
                LearningRate = request.LearningRate ?? 2e-5,
                BatchSize = request.BatchSize ?? 4,
                WarmupRatio = 0.1,
                WeightDecay = 0.01,
                LoraRank = request.LoraRank ?? 16,
                LoraAlpha = request.LoraAlpha ?? 32,
                UseLora = request.UseLora ?? true
            },
            TrainingSamples = validation.SampleCount,
            CreatedAt = DateTime.UtcNow
        };

        await _jobQueue.EnqueueAsync(job, ct);

        return job;
    }

    private DatasetValidation ValidateDataset(byte[] data)
    {
        var lines = Encoding.UTF8.GetString(data)
            .Split('\n', StringSplitOptions.RemoveEmptyEntries);

        var sampleCount = 0;
        var piiDetected = false;
        var errors = new List<string>();

        foreach (var line in lines)
        {
            var doc = JsonSerializer.Deserialize<TrainingDocument>(line);
            if (doc == null || doc.Messages == null || doc.Messages.Count == 0)
            {
                errors.Add("Invalid document format");
                continue;
            }

            if (doc.Messages.Count < 2)
                errors.Add("Document must have at least 2 messages");

            if (doc.Messages.Any(m => m.Content?.Length > 32000))
                errors.Add("Message exceeds maximum length of 32000 characters");

            if (PiiDetector.ContainsPii(doc.Messages))
                piiDetected = true;

            sampleCount++;
        }

        return new DatasetValidation
        {
            IsValid = errors.Count == 0,
            SampleCount = sampleCount,
            ContainsPii = piiDetected,
            ErrorMessages = errors
        };
    }
}

Training Configuration

HyperparameterDefaultRangeDescription
Epochs31-10Number of complete passes through training data
Learning Rate2e-51e-6 to 5e-5Peak learning rate with cosine schedule
Batch Size41-32Per-GPU batch size with gradient accumulation
LoRA Rank164-64Rank of low-rank adaptation matrices
LoRA Alpha328-128Scaling factor for LoRA updates
Warmup Ratio0.10-0.3Fraction of steps for learning rate warmup
Max Seq Length4096512-131072Maximum sequence length for training

10. Prompt Management and Versioning

Enterprise teams need to collaborate on prompts, test variations, and maintain version history. The prompt management system provides a Git-like workflow for prompt templates with branching, versioning, and rollback capabilities.

Prompt Template Structure

C#
public class PromptTemplate
{
    public Guid Id { get; set; }
    public Guid OrgId { get; set; }
    public string Name { get; set; }
    public int Version { get; set; }
    public string SystemPrompt { get; set; }
    public List<PromptParameter> Parameters { get; set; }
    public string DefaultModel { get; set; }
    public List<FewShotExample> FewShotExamples { get; set; }
    public Dictionary<string, string> Variables { get; set; }
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? PublishedAt { get; set; }
}

public class PromptParameter
{
    public string Name { get; set; }
    public string Type { get; set; } // string, int, float, bool
    public string DefaultValue { get; set; }
    public string Description { get; set; }
    public bool IsRequired { get; set; }
}

public class PromptService : IPromptService
{
    private readonly IPromptRepository _repo;

    public async Task<PromptRenderResult> RenderAsync(
        string templateName,
        int? version,
        Dictionary<string, object> variables,
        Guid orgId,
        CancellationToken ct)
    {
        var template = version.HasValue
            ? await _repo.GetByVersionAsync(orgId, templateName, version.Value, ct)
            : await _repo.GetActiveVersionAsync(orgId, templateName, ct);

        if (template == null)
            throw new NotFoundException($"Prompt template '{templateName}' not found");

        var systemPrompt = template.SystemPrompt;
        foreach (var kv in template.Variables)
        {
            var value = variables.ContainsKey(kv.Key)
                ? variables[kv.Key]?.ToString() ?? kv.Value
                : kv.Value;
            systemPrompt = systemPrompt.Replace($"{{{{{kv.Key}}}}}", value);
        }

        foreach (var param in template.Parameters.Where(p => p.IsRequired))
        {
            if (!variables.ContainsKey(param.Name) || variables[param.Name] == null)
                throw new ValidationException(
                    $"Required parameter '{param.Name}' is missing");
        }

        return new PromptRenderResult
        {
            SystemPrompt = systemPrompt,
            FewShotExamples = template.FewShotExamples,
            Model = template.DefaultModel,
            Version = template.Version
        };
    }
}
Best Practice: Always pin prompt template versions in production code. Use the latest version only in development. This prevents unexpected behavior changes when a team member updates a prompt template.

11. Token Billing and Usage Tracking

Accurate token billing is essential for enterprise platforms. Every request must be metered precisely, and usage must be attributed to the correct organization and user. We use an event-driven architecture with Kafka to handle high-volume usage recording without impacting request latency.

Billing Architecture

graph LR Req["Inference Request"] BillingSvc["Billing Service"] Kafka["Kafka
usage-events"] Aggregator["Usage Aggregator"] PG["PostgreSQL
usage_records"] Redis["Redis
real-time counters"] QuotaEnforcer["Quota Enforcer"] Req -->|"on complete"| BillingSvc BillingSvc -->|"publish event"| Kafka Kafka --> Aggregator Aggregator --> PG Kafka --> Redis Redis --> QuotaEnforcer

Token Counting

Token counting must match the model's tokenizer exactly. We embed the tokenizer for each model and count tokens server-side rather than relying on client-reported counts. This prevents billing discrepancies and ensures accurate metering even when clients use different tokenization libraries.

C#
public class TokenBillingService : IBillingService
{
    private readonly IKafkaProducer _kafka;
    private readonly ITokenCounter _tokenizer;
    private readonly IRedisCache _cache;
    private readonly IOrganizationRepository _orgs;

    public async Task<QuotaCheckResult> CheckQuotaAsync(
        Guid orgId,
        string modelSlug,
        CancellationToken ct)
    {
        var cacheKey = $"quota:{orgId}:monthly";
        var used = await _cache.GetAsync<long>(cacheKey, ct);

        if (used.HasValue)
        {
            var org = await _orgs.GetAsync(orgId, ct);
            return new QuotaCheckResult
            {
                HasCapacity = used.Value < org.MonthlyTokenQuota,
                Remaining = org.MonthlyTokenQuota - used.Value
            };
        }

        var dbUsage = await _orgs.GetMonthlyUsageAsync(orgId, ct);
        await _cache.SetAsync(cacheKey, dbUsage, TimeSpan.FromMinutes(5), ct);

        var orgData = await _orgs.GetAsync(orgId, ct);
        return new QuotaCheckResult
        {
            HasCapacity = dbUsage < orgData.MonthlyTokenQuota,
            Remaining = orgData.MonthlyTokenQuota - dbUsage
        };
    }

    public async Task RecordUsageAsync(
        UsageRecord record,
        CancellationToken ct)
    {
        record.Id = Guid.NewGuid();
        record.RecordedAt = DateTime.UtcNow;
        record.CostCents = CalculateCost(record.ModelSlug,
            record.InputTokens, record.OutputTokens);

        await _kafka.PublishAsync("usage-events", record.Id.ToString(),
            record, ct);

        var cacheKey = $"quota:{record.OrgId}:monthly";
        await _cache.IncrementAsync(cacheKey,
            record.InputTokens + record.OutputTokens, ct);
    }

    private decimal CalculateCost(string model, int input, int output)
    {
        var pricing = ModelPricing.Get(model);
        return input * pricing.InputPerToken + output * pricing.OutputPerToken;
    }
}

public static class ModelPricing
{
    private static readonly Dictionary<string, PricingTier> _pricing = new()
    {
        ["llama-3.1-8b"] = new(0.0000001m, 0.0000002m),
        ["llama-3.1-70b"] = new(0.0000008m, 0.0000024m),
        ["llama-3.1-405b"] = new(0.000004m, 0.000012m),
        ["mistral-large"] = new(0.000003m, 0.000009m),
        ["text-embedding-3-small"] = new(0.00000002m, 0m),
        ["text-embedding-3-large"] = new(0.00000013m, 0m)
    };

    public static PricingTier Get(string model) =>
        _pricing.TryGetValue(model, out var p) ? p : new(0.000001m, 0.000002m);
}

Pricing Table

ModelInput (per 1M tokens)Output (per 1M tokens)
Llama 3.1 8B$0.10$0.20
Llama 3.1 70B$0.80$2.40
Llama 3.1 405B$4.00$12.00
Mistral Large$3.00$9.00
Embedding 3 Small$0.02N/A
Embedding 3 Large$0.13N/A

12. Safety and Content Moderation

Enterprise platforms must prevent harmful, toxic, or policy-violating content from both inputs and outputs. The safety system operates as a pipeline of classifiers that run in parallel with minimal latency impact.

Safety Pipeline

graph LR Input["User Input"] Keyword["Keyword Filter
(1ms)"] Toxicity["Toxicity Classifier
(15ms)"] PII["PII Detector
(5ms)"] Policy["Policy Model
(30ms)"] Decision["Safety Decision"] LLM["LLM Inference"] OutputFilter["Output Filter
(parallel with streaming)"] Response["Filtered Response"] Input --> Keyword Input --> PII Keyword --> Decision Toxicity --> Decision PII --> Decision Policy --> Decision Decision -->|"pass"| LLM Decision -->|"block"| Response LLM --> OutputFilter OutputFilter --> Response

Content Safety Service

C#
public class ContentSafetyService : ISafetyService
{
    private readonly IKeywordFilter _keywordFilter;
    private readonly IToxicityClassifier _toxicity;
    private readonly IPiiDetector _pii;
    private readonly IPolicyModel _policy;
    private readonly ILogger<ContentSafetyService> _logger;

    public ContentSafetyService(
        IKeywordFilter keywordFilter,
        IToxicityClassifier toxicity,
        IPiiDetector pii,
        IPolicyModel policy,
        ILogger<ContentSafetyService> logger)
    {
        _keywordFilter = keywordFilter;
        _toxicity = toxicity;
        _pii = pii;
        _policy = policy;
        _logger = logger;
    }

    public async Task<SafetyResult> CheckInputAsync(
        List<ChatMessage> messages,
        Guid orgId,
        CancellationToken ct)
    {
        var text = string.Join(" ", messages.Select(m => m.Content));
        var tasks = new List<Task<SafetySignal>>
        {
            CheckKeywordsAsync(text, ct),
            _toxicity.ClassifyAsync(text, ct),
            _pii.DetectAsync(text, ct),
            _policy.EvaluateAsync(text, orgId, ct)
        };

        var results = await Task.WhenAll(tasks);

        var blocked = results.FirstOrDefault(r => r.Severity == Severity.Block);
        if (blocked != null)
        {
            _logger.LogWarning(
                "Input blocked for org {OrgId}: {Reason}",
                orgId, blocked.Category);
            return new SafetyResult
            {
                IsBlocked = true,
                BlockReason = $"Content policy violation: {blocked.Category}"
            };
        }

        var flagged = results.Where(r => r.Severity == Severity.Flag).ToList();
        return new SafetyResult
        {
            IsBlocked = false,
            Flags = flagged
        };
    }

    public async Task<SafetyResult> CheckOutputAsync(
        List<Choice> choices,
        Guid orgId,
        CancellationToken ct)
    {
        var text = string.Join(" ", choices.Select(c => c.Message.Content));
        var toxicity = await _toxicity.ClassifyAsync(text, ct);
        var pii = await _pii.DetectAsync(text, ct);

        if (toxicity.Severity == Severity.Block || pii.ContainsSensitiveData)
        {
            return new SafetyResult
            {
                IsBlocked = true,
                BlockReason = "Output failed safety check"
            };
        }

        return new SafetyResult { IsBlocked = false };
    }

    private async Task<SafetySignal> CheckKeywordsAsync(
        string text, CancellationToken ct)
    {
        var result = await _keywordFilter.CheckAsync(text, ct);
        return result.AnyBlocked
            ? new SafetySignal(Severity.Block, "banned_terms", 1.0)
            : new SafetySignal(Severity.Pass, "", 0);
    }
}

The safety system must be configurable per tenant. Some organizations may want stricter content policies than the platform defaults, while others may need to allow domain-specific terminology that would normally trigger filters. Each tenant can configure policy overrides in their organization settings.

13. Retrieval-Augmented Generation (RAG)

RAG is the most impactful feature for enterprise customers because it allows LLMs to answer questions based on proprietary documents without fine-tuning. The RAG pipeline ingests documents, chunks them intelligently, generates embeddings, stores them in a vector database, and retrieves relevant chunks at query time.

RAG Pipeline Architecture

graph TB Docs["Document Upload"] Parser["Document Parser
PDF, DOCX, HTML, MD"] Chunker["Smart Chunker
Semantic Splitting"] Embedder["Embedding Service
text-embedding-3-large"] VectorStore["Vector Store
pgvector / Weaviate"] Query["User Query"] QEmbed["Query Embedding"] Retriever["Hybrid Retriever
Semantic + BM25"] Reranker["Reranker
Cross-encoder"] PromptBuilder["Prompt Builder
Context Injection"] LLM["LLM Inference"] Docs --> Parser Parser --> Chunker Chunker --> Embedder Embedder --> VectorStore Query --> QEmbed QEmbed --> Retriever VectorStore --> Retriever Retriever --> Reranker Reranker --> PromptBuilder PromptBuilder --> LLM

Document Chunking Strategy

C#
public class SemanticChunker : IDocumentChunker
{
    private readonly IEmbeddingService _embeddings;
    private readonly int _targetChunkSize;
    private readonly int _overlapSize;
    private readonly double _similarityThreshold;

    public SemanticChunker(
        IEmbeddingService embeddings,
        int targetChunkSize = 512,
        int overlapSize = 50,
        double similarityThreshold = 0.75)
    {
        _embeddings = embeddings;
        _targetChunkSize = targetChunkSize;
        _overlapSize = overlapSize;
        _similarityThreshold = similarityThreshold;
    }

    public async Task<List<DocumentChunk>> ChunkAsync(
        Document document,
        CancellationToken ct)
    {
        var paragraphs = SplitIntoParagraphs(document.Content);
        var sentences = paragraphs.SelectMany(SplitIntoSentences).ToList();
        var embeddings = await _embeddings.EmbedBatchAsync(
            sentences.Select(s => s.Text).ToList(), ct);

        var chunks = new List<DocumentChunk>();
        var currentChunk = new List<Sentence>();
        var currentTokenCount = 0;

        for (int i = 0; i < sentences.Count; i++)
        {
            currentChunk.Add(sentences[i]);
            currentTokenCount += sentences[i].TokenCount;

            if (currentTokenCount >= _targetChunkSize)
            {
                var nextEmbedding = i + 1 < embeddings.Count
                    ? embeddings[i + 1] : null;

                if (nextEmbedding != null)
                {
                    var similarity = CosineSimilarity(
                        embeddings[i], nextEmbedding);

                    if (similarity < _similarityThreshold || currentTokenCount >= _targetChunkSize * 1.5)
                    {
                        chunks.Add(CreateChunk(document, currentChunk));
                        var overlap = currentChunk.TakeLast(
                            _overlapSize).ToList();
                        currentChunk = new List<Sentence>(overlap);
                        currentTokenCount = overlap.Sum(s => s.TokenCount);
                    }
                }
                else
                {
                    chunks.Add(CreateChunk(document, currentChunk));
                    currentChunk = new List<Sentence>();
                    currentTokenCount = 0;
                }
            }
        }

        if (currentChunk.Count > 0)
            chunks.Add(CreateChunk(document, currentChunk));

        return chunks;
    }

    private DocumentChunk CreateChunk(
        Document doc, List<Sentence> sentences)
    {
        return new DocumentChunk
        {
            Id = Guid.NewGuid(),
            DocumentId = doc.Id,
            Content = string.Join(" ", sentences.Select(s => s.Text)),
            TokenCount = sentences.Sum(s => s.TokenCount),
            StartSentence = sentences.First().Index,
            EndSentence = sentences.Last().Index,
            Metadata = doc.Metadata
        };
    }
}

Retrieval Strategy

We use a hybrid retrieval approach combining semantic search with BM25 keyword matching. The semantic search captures conceptual similarity while BM25 ensures exact keyword matches are not missed. Results from both strategies are merged using Reciprocal Rank Fusion (RRF) and then reranked using a cross-encoder model for precision.

C#
public class HybridRetriever : IRetriever
{
    private readonly IVectorSearch _vectorSearch;
    private readonly IBm25Search _bm25Search;
    private readonly IReranker _reranker;

    public async Task<List<RetrievedChunk>> RetrieveAsync(
        string query,
        Guid kbId,
        int topK,
        CancellationToken ct)
    {
        var semanticTask = _vectorSearch.SearchAsync(
            query, kbId, topK * 3, ct);
        var bm25Task = _bm25Search.SearchAsync(
            query, kbId, topK * 3, ct);

        await Task.WhenAll(semanticTask, bm25Task);

        var semanticResults = await semanticTask;
        var bm25Results = await bm25Task;

        var fused = ReciprocalRankFusion(
            semanticResults, bm25Results, k: 60);

        var reranked = await _reranker.RerankAsync(
            query, fused.Take(topK * 2).ToList(), topK, ct);

        return reranked;
    }

    private List<ScoredChunk> ReciprocalRankFusion(
        List<ScoredChunk> a,
        List<ScoredChunk> b,
        int k)
    {
        var scores = new Dictionary<Guid, double>();
        var chunks = new Dictionary<Guid, ScoredChunk>();

        for (int i = 0; i < a.Count; i++)
        {
            scores.TryAdd(a[i].ChunkId, 0);
            scores[a[i].ChunkId] += 1.0 / (k + i + 1);
            chunks.TryAdd(a[i].ChunkId, a[i]);
        }

        for (int i = 0; i < b.Count; i++)
        {
            scores.TryAdd(b[i].ChunkId, 0);
            scores[b[i].ChunkId] += 1.0 / (k + i + 1);
            chunks.TryAdd(b[i].ChunkId, b[i]);
        }

        return scores
            .OrderByDescending(kv => kv.Value)
            .Select(kv => new ScoredChunk
            {
                ChunkId = kv.Key,
                Score = kv.Value,
                Chunk = chunks[kv.Key].Chunk
            })
            .ToList();
    }
}

14. Embeddings and Vector Store

Embeddings are numerical representations of text that capture semantic meaning. The platform supports multiple embedding models and vector database backends to give customers flexibility in choosing the right trade-off between quality, speed, and cost.

Supported Embedding Models

ModelDimensionsMax TokensCost per 1M Tokens
text-embedding-3-small15368191$0.02
text-embedding-3-large30728191$0.13
nomic-embed-text7688192Self-hosted
bge-large-en-v1.51024512Self-hosted

Vector Store Configuration

C#
public class VectorStoreService : IVectorStore
{
    private readonly NpgsqlConnection _connection;

    public VectorStoreService(NpgsqlConnection connection)
    {
        _connection = connection;
    }

    public async Task InsertVectorsAsync(
        List<VectorEntry> entries,
        CancellationToken ct)
    {
        const string sql = @"
            INSERT INTO vector_embeddings
                (id, kb_id, chunk_id, embedding, metadata)
            VALUES
                (@id, @kbId, @chunkId, @embedding, @metadata::jsonb)";

        foreach (var entry in entries)
        {
            await using var cmd = new NpgsqlCommand(sql, _connection);
            cmd.Parameters.AddWithValue("id", entry.Id);
            cmd.Parameters.AddWithValue("kbId", entry.KnowledgeBaseId);
            cmd.Parameters.AddWithValue("chunkId", entry.ChunkId);
            cmd.Parameters.AddWithValue("embedding", entry.Vector);
            cmd.Parameters.AddWithValue("metadata",
                JsonSerializer.Serialize(entry.Metadata));
            await cmd.ExecuteNonQueryAsync(ct);
        }
    }

    public async Task<List<VectorSearchResult>> SearchAsync(
        float[] queryVector,
        Guid kbId,
        int topK,
        double minScore,
        CancellationToken ct)
    {
        const string sql = @"
            SELECT id, chunk_id, metadata,
                   1 - (embedding <=> @queryVector) AS score
            FROM vector_embeddings
            WHERE kb_id = @kbId
              AND 1 - (embedding <=> @queryVector) >= @minScore
            ORDER BY embedding <=> @queryVector
            LIMIT @topK";

        await using var cmd = new NpgsqlCommand(sql, _connection);
        cmd.Parameters.AddWithValue("queryVector", queryVector);
        cmd.Parameters.AddWithValue("kbId", kbId);
        cmd.Parameters.AddWithValue("topK", topK);
        cmd.Parameters.AddWithValue("minScore", minScore);

        var results = new List<VectorSearchResult>();
        await using var reader = await cmd.ExecuteReaderAsync(ct);
        while (await reader.ReadAsync(ct))
        {
            results.Add(new VectorSearchResult
            {
                Id = reader.GetGuid(0),
                ChunkId = reader.GetGuid(1),
                Metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(
                    reader.GetString(2)),
                Score = reader.GetDouble(3)
            });
        }

        return results;
    }
}

We use pgvector for small to medium deployments (up to 100 million vectors) because it integrates directly with PostgreSQL and avoids the operational complexity of a separate vector database. For larger deployments, we support Weaviate and Pinecone as alternative backends through the same IVectorStore interface.

15. Rate Limiting and Quotas

Rate limiting protects the platform from abuse, ensures fair resource allocation, and prevents any single tenant from degrading performance for others. We implement a multi-layer rate limiting strategy.

Rate Limiting Layers

LayerScopeMechanismGranularity
API GatewayPer API KeyToken BucketPer second
OrchestrationPer OrganizationSliding WindowPer minute
BillingPer OrganizationCounterMonthly
InferencePer ModelSemaphoreConcurrent requests
Fine-TuningPer OrganizationQueue PriorityPer job

Token Bucket Implementation

C#
public class RedisRateLimiter : IRateLimiter
{
    private readonly IRedisCache _redis;

    private const string LuaScript = @"
        local key = KEYS[1]
        local max_tokens = tonumber(ARGV[1])
        local refill_rate = tonumber(ARGV[2])
        local now = tonumber(ARGV[3])
        local requested = tonumber(ARGV[4])

        local bucket = redis.call('hmget', key,
            'tokens', 'last_refill')
        local tokens = tonumber(bucket[1]) or max_tokens
        local last_refill = tonumber(bucket[2]) or now

        local elapsed = now - last_refill
        tokens = math.min(max_tokens,
            tokens + elapsed * refill_rate)

        if tokens >= requested then
            tokens = tokens - requested
            redis.call('hmset', key,
                'tokens', tokens,
                'last_refill', now)
            redis.call('expire', key,
                math.ceil(max_tokens / refill_rate) * 2)
            return {1, tokens}
        else
            redis.call('hmset', key,
                'tokens', tokens,
                'last_refill', now)
            return {0, tokens}
        end";

    public RedisRateLimiter(IRedisCache redis)
    {
        _redis = redis;
    }

    public async Task<RateLimitResult> CheckAsync(
        string key,
        RateLimitConfig config,
        CancellationToken ct)
    {
        var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0;
        var result = await _redis.EvalAsync<long[]>(
            LuaScript,
            new[] { $"ratelimit:{key}" },
            new object[]
            {
                config.MaxTokens,
                config.RefillRatePerSecond,
                now,
                1
            }, ct);

        return new RateLimitResult
        {
            Allowed = result[0] == 1,
            RemainingTokens = (int)result[1],
            RetryAfterMs = result[0] == 0
                ? (int)(1000 / config.RefillRatePerSecond)
                : 0
        };
    }
}

When a request exceeds the rate limit, the API gateway returns a 429 status code with Retry-After and X-RateLimit-Remaining headers. The platform also supports burst allowances for tenants on higher-tier plans, allowing short spikes in traffic without being throttled.

16. Multi-Tenant Workspace Isolation

Enterprise customers demand strict data isolation. The platform ensures that one organization's data, prompts, knowledge bases, and usage records are completely invisible to other organizations at every layer of the stack.

Isolation Boundaries

LayerIsolation Mechanism
API GatewayJWT token contains org_id, validated on every request
DatabaseRow-level security policies filter by org_id
Vector StoreNamespaces/partitions per knowledge base (org-scoped)
Object StoragePrefix-based isolation: /org/{id}/files/
InferenceOptional dedicated model instances for premium tenants
Fine-TuningSeparate training jobs, isolated model registries
CacheKey-prefixed by org_id: org:{id}:session:{sid}

Workspace Service

C#
public class WorkspaceMiddleware
{
    private readonly RequestDelegate _next;

    public WorkspaceMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var token = context.Request.Headers.Authorization
            .FirstOrDefault()?.Replace("Bearer ", "");

        if (string.IsNullOrEmpty(token))
        {
            context.Response.StatusCode = 401;
            return;
        }

        var principal = ValidateJwt(token);
        var orgId = principal.FindFirst("org_id")?.Value;

        if (string.IsNullOrEmpty(orgId))
        {
            context.Response.StatusCode = 401;
            return;
        }

        context.Items["OrgId"] = Guid.Parse(orgId);
        context.Items["UserId"] = Guid.Parse(
            principal.FindFirst("sub")?.Value);
        context.Items["UserRole"] = principal.FindFirst("role")?.Value;
        context.Items["OrgPlan"] = principal.FindFirst("plan")?.Value;

        context.Response.Headers.Add("X-Org-Id", orgId);

        await _next(context);
    }
}

public static class HttpContextExtensions
{
    public static Guid GetOrganizationId(this HttpContext context)
        => (Guid)context.Items["OrgId"]!;

    public static Guid GetUserId(this HttpContext context)
        => (Guid)context.Items["UserId"]!;

    public static string GetUserRole(this HttpContext context)
        => (string)context.Items["UserRole"]!;
}
Security Critical: Every database query in the platform must include an org_id filter. Use PostgreSQL row-level security policies as a defense-in-depth measure in addition to application-level filtering. This ensures that even if a code bug bypasses the application filter, the database will still enforce isolation.

17. Model Versioning and A/B Testing

Enterprises need to manage multiple model versions, test new models against existing ones, and gradually roll out updates. The platform provides model versioning with routing rules that support canary deployments, A/B testing, and shadow mode.

Routing Rules

C#
public class ModelRouter : IModelRouter
{
    private readonly IModelRepository _models;
    private readonly ITrafficSplitter _splitter;
    private readonly ILogger<ModelRouter> _logger;

    public async Task<InferenceBackend> RouteAsync(
        string modelSlug,
        Guid orgId,
        RequestContext context,
        CancellationToken ct)
    {
        var versions = await _models.GetVersionsAsync(
            modelSlug, ct);

        if (!versions.Any())
            throw new NotFoundException(
                $"No active versions for model '{modelSlug}'");

        var rules = await _models.GetRoutingRulesAsync(
            modelSlug, orgId, ct);

        foreach (var rule in rules.OrderByDescending(r => r.Priority))
        {
            if (rule.Matches(context))
            {
                var target = _splitter.SelectTarget(
                    rule.Targets, orgId, context);

                _logger.LogInformation(
                    "Routing {Model} to {Target} via rule {Rule}",
                    modelSlug, target.Version, rule.Name);

                return await GetBackendAsync(target, ct);
            }
        }

        var defaultVersion = versions.First(v => v.IsDefault);
        return await GetBackendAsync(defaultVersion, ct);
    }
}

public class RoutingRule
{
    public string Name { get; set; }
    public int Priority { get; set; }
    public List<TrafficTarget> Targets { get; set; }
    public RoutingCondition Condition { get; set; }

    public bool Matches(RequestContext context)
    {
        if (Condition == null) return true;

        if (Condition.UserIds?.Any() == true
            && !Condition.UserIds.Contains(context.UserId))
            return false;

        if (Condition.Percentage < 100)
        {
            var hash = HashCode.Combine(
                context.UserId, context.RequestId) % 100;
            if (hash >= Condition.Percentage) return false;
        }

        return true;
    }
}

public class TrafficTarget
{
    public string Version { get; set; }
    public int Weight { get; set; }
    public bool IsShadow { get; set; }
}

A/B Testing Framework

The A/B testing framework routes a percentage of traffic to different model versions and collects latency, quality, and safety metrics for each variant. This allows teams to make data-driven decisions about model upgrades. Shadow mode routes traffic to a new model version without returning its response to the user, logging the shadow response for offline comparison.

18. Knowledge Base Management

Knowledge bases organize documents for RAG. Each knowledge base is an isolated collection of documents with its own embedding model, chunking configuration, and access controls.

C#
public class KnowledgeBaseService : IKnowledgeBaseService
{
    private readonly IKnowledgeBaseRepository _repo;
    private readonly IDocumentParser _parser;
    private readonly IDocumentChunker _chunker;
    private readonly IEmbeddingService _embeddings;
    private readonly IVectorStore _vectorStore;
    private readonly IBlobStorage _storage;

    public async Task<KnowledgeBase> CreateAsync(
        CreateKnowledgeBaseRequest request,
        Guid orgId,
        CancellationToken ct)
    {
        var kb = new KnowledgeBase
        {
            Id = Guid.NewGuid(),
            OrgId = orgId,
            Name = request.Name,
            Description = request.Description,
            EmbeddingModel = request.EmbeddingModel
                ?? "text-embedding-3-small",
            ChunkSize = request.ChunkSize ?? 512,
            ChunkOverlap = request.ChunkOverlap ?? 50,
            CreatedAt = DateTime.UtcNow
        };

        await _repo.CreateAsync(kb, ct);
        return kb;
    }

    public async Task<DocumentUploadResult> UploadDocumentAsync(
        Guid kbId,
        IFormFile file,
        Guid orgId,
        CancellationToken ct)
    {
        var kb = await _repo.GetAsync(kbId, ct);
        if (kb == null || kb.OrgId != orgId)
            throw new NotFoundException("Knowledge base not found");

        var filePath = $"org/{orgId}/kb/{kbId}/{file.FileName}";
        await using var stream = file.OpenReadStream();
        await _storage.UploadAsync(filePath, stream, ct);

        var document = new KbDocument
        {
            Id = Guid.NewGuid(),
            KbId = kbId,
            Filename = file.FileName,
            MimeType = file.ContentType,
            FileSizeBytes = file.Length,
            Status = "processing"
        };
        await _repo.CreateDocumentAsync(document, ct);

        _ = ProcessDocumentAsync(document, kb, filePath, ct);

        return new DocumentUploadResult
        {
            DocumentId = document.Id,
            Status = "processing"
        };
    }

    private async Task ProcessDocumentAsync(
        KbDocument document,
        KnowledgeBase kb,
        string filePath,
        CancellationToken ct)
    {
        try
        {
            var content = await _storage.DownloadAsync(filePath, ct);
            var parsed = await _parser.ParseAsync(
                content, document.MimeType, ct);
            var chunks = await _chunker.ChunkAsync(
                new Document
                {
                    Id = document.Id,
                    Content = parsed.Text,
                    Metadata = parsed.Metadata
                }, ct);

            var texts = chunks.Select(c => c.Content).ToList();
            var vectors = await _embeddings.EmbedBatchAsync(texts, ct);

            var entries = chunks.Zip(vectors).Select(pair =>
                new VectorEntry
                {
                    Id = Guid.NewGuid(),
                    KnowledgeBaseId = kb.Id,
                    ChunkId = pair.First.Id,
                    Vector = pair.Second,
                    Metadata = pair.First.Metadata
                }).ToList();

            await _vectorStore.InsertVectorsAsync(entries, ct);

            document.ChunkCount = chunks.Count;
            document.Status = "ready";
            await _repo.UpdateDocumentAsync(document, ct);

            kb.DocumentCount++;
            kb.TotalChunks += chunks.Count;
            await _repo.UpdateAsync(kb, ct);
        }
        catch (Exception ex)
        {
            document.Status = "failed";
            document.Metadata["error"] = ex.Message;
            await _repo.UpdateDocumentAsync(document, ct);
        }
    }
}

Supported Document Formats

FormatParserMax Size
PDFApache PDFBox / pdfplumber100 MB
DOCXOpen XML SDK50 MB
HTMLHtmlAgilityPack + Readability10 MB
MarkdownMarkdig10 MB
Plain TextStreamReader10 MB
CSVCsvHelper50 MB
JSONSystem.Text.Json50 MB

19. Admin Dashboard and Observability

The admin dashboard is a React single-page application that provides platform administrators and tenant admins with visibility into system health, usage patterns, and configuration management.

Dashboard Features

  • Real-time Metrics: Requests per second, average latency, token throughput, error rates, and GPU utilization graphs updated every 5 seconds.
  • Usage Analytics: Token consumption trends, cost breakdown by model, user activity heatmaps, and department-level rollups.
  • Tenant Management: Create, update, and deactivate organizations. Manage API keys, quotas, and billing plans.
  • Prompt Playground: Interactive prompt testing environment with model selection, parameter tuning, and side-by-side comparison of prompt versions.
  • Audit Log Viewer: Searchable, filterable log of all platform activities with export to CSV and integration with SIEM systems.
  • Safety Dashboard: Content moderation statistics, blocked request trends, and policy configuration management.

Admin API Endpoints

C#
[ApiController]
[Route("admin/v1")]
[Authorize(Roles = "platform_admin")]
public class AdminController : ControllerBase
{
    private readonly IOrganizationRepository _orgs;
    private readonly IUsageAnalytics _analytics;
    private readonly IAuditLog _auditLog;

    [HttpGet("organizations")]
    public async Task<IActionResult> ListOrganizations(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 25,
        CancellationToken ct = default)
    {
        var result = await _orgs.ListAsync(page, pageSize, ct);
        return Ok(new
        {
            organizations = result.Items,
            total = result.TotalCount,
            page,
            pageSize
        });
    }

    [HttpGet("usage/summary")]
    public async Task<IActionResult> GetUsageSummary(
        [FromQuery] DateTime from,
        [FromQuery] DateTime to,
        [FromQuery] string groupBy = "organization",
        CancellationToken ct = default)
    {
        var summary = await _analytics.GetSummaryAsync(
            from, to, groupBy, ct);
        return Ok(summary);
    }

    [HttpGet("system/health")]
    public async Task<IActionResult> GetSystemHealth(
        CancellationToken ct)
    {
        var health = new
        {
            inference = await CheckInferenceBackends(ct),
            database = await CheckDatabase(ct),
            redis = await CheckRedis(ct),
            kafka = await CheckKafka(ct),
            vectorStore = await CheckVectorStore(ct),
            gpu = await CheckGpuHealth(ct)
        };
        return Ok(health);
    }

    [HttpGet("audit-log")]
    public async Task<IActionResult> SearchAuditLog(
        [FromQuery] AuditLogQuery query,
        CancellationToken ct)
    {
        var entries = await _auditLog.SearchAsync(query, ct);
        return Ok(entries);
    }
}

20. Monitoring, Logging, and Alerting

Comprehensive observability is non-negotiable for an enterprise LLM platform. We implement the three pillars of observability: metrics, logs, and traces, using Prometheus, Loki, and Jaeger.

Key Metrics

MetricTypeLabelsAlert Threshold
llm_requests_totalCountermodel, org, statusError rate > 1%
llm_latency_secondsHistogrammodel, operationp99 > 5s
llm_tokens_totalCountermodel, org, directionN/A
llm_first_token_msHistogrammodelp95 > 1s
gpu_utilization_percentGaugeinstance, model< 30% for 10min
gpu_memory_used_percentGaugeinstance> 95%
inference_queue_depthGaugemodel> 100
embedding_latency_msHistogrammodelp99 > 500ms
rag_retrieval_latency_msHistogramkbp99 > 200ms
safety_checks_totalCountertype, resultBlock rate > 5%

Structured Logging

C#
public class CompletionTelemetryMiddleware
{
    private readonly RequestDelegate _next;

    public CompletionTelemetryMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        var requestId = Guid.NewGuid().ToString();

        context.Request.Headers["X-Request-Id"] = requestId;

        try
        {
            await _next(context);
            sw.Stop();

            var orgId = context.GetOrganizationId();
            var model = context.Items["ModelSlug"]?.ToString();

            Log.Information(
                "Request completed {RequestId} " +
                "Org={OrgId} Model={Model} " +
                "Status={Status} LatencyMs={LatencyMs} " +
                "InputTokens={InputTokens} OutputTokens={OutputTokens}",
                requestId, orgId, model,
                context.Response.StatusCode,
                sw.ElapsedMilliseconds,
                context.Items["InputTokens"] ?? 0,
                context.Items["OutputTokens"] ?? 0);

            Metrics.RecordRequest(model, orgId,
                context.Response.StatusCode,
                sw.ElapsedMilliseconds);
        }
        catch (Exception ex)
        {
            sw.Stop();
            Log.Error(ex,
                "Request failed {RequestId} LatencyMs={LatencyMs}",
                requestId, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

Alerting Rules

YAML
groups:
  - name: llm-platform
    rules:
      - alert: HighErrorRate
        expr: |
          rate(llm_requests_total{status=~"5.."}[5m])
          / rate(llm_requests_total[5m]) > 0.01
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "LLM platform error rate exceeds 1%"

      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(llm_latency_seconds_bucket[5m])) > 5
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "LLM p99 latency exceeds 5 seconds"

      - alert: GPUMemoryHigh
        expr: gpu_memory_used_percent > 95
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "GPU memory utilization above 95%"

      - alert: InferenceQueueBacklog
        expr: inference_queue_depth > 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Inference queue depth exceeds 100"

Distributed tracing with Jaeger or OpenTelemetry allows us to follow a request through the entire system, from API gateway through orchestration, RAG retrieval, safety checks, and inference. This is invaluable for debugging latency issues in production.

21. Enterprise SSO and Compliance

Enterprise customers require single sign-on integration with their identity providers. The platform supports SAML 2.0, OpenID Connect, and SCIM for automated user provisioning.

SSO Integration Architecture

graph TB IdP["Identity Provider
(Azure AD, Okta, etc.)"] SAML["SAML 2.0 / OIDC
Handler"] SCIM["SCIM Provisioner
Auto user sync"] Platform["LLM Platform
Auth Service"] JWT["JWT Token Service"] IdP --> SAML SAML --> Platform IdP --> SCIM SCIM --> Platform Platform --> JWT

Compliance Requirements

ComplianceRequirementsImplementation
SOC 2 Type IIAccess controls, audit logging, encryptionRBAC, full audit trail, AES-256 at rest, TLS 1.3 in transit
HIPAAPHI protection, BAAs, audit controlsEncryption, access logs, data retention policies, BAA workflow
GDPRData subject rights, consent, portabilityData export API, deletion workflows, consent management
EU AI ActTransparency, risk classification, human oversightAI content labeling, risk assessment tools, escalation workflows
ISO 27001Information security managementSecurity controls documentation, risk assessments, incident response
C#
public class SamlAuthHandler : IExternalAuthProvider
{
    private readonly Saml2Configuration _config;

    public async Task<AuthResult> AuthenticateAsync(
        SamlResponse response,
        CancellationToken ct)
    {
        var assertions = ValidateAndExtractAssertions(response);

        var email = assertions.GetAttributeValue(
            "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
        var name = assertions.GetAttributeValue(
            "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name");
        var groups = assertions.GetMultiValueAttribute(
            "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups");

        var user = await _users.FindByEmailAsync(email, ct);
        if (user == null)
        {
            var org = await _orgs.FindBySsoDomainAsync(
                ExtractDomain(email), ct);
            if (org == null)
                return AuthResult.Failed("Organization not found");

            user = await ProvisionUserAsync(
                org.Id, email, name, "member", ct);
        }

        await SyncGroupMembership(user, groups, ct);
        await _auditLog.RecordAsync(new AuditEntry
        {
            Action = "sso.login",
            UserId = user.Id,
            OrgId = user.OrgId,
            Metadata = new Dictionary<string, object>
            {
                ["provider"] = "saml",
                ["ip"] = _httpContext.Connection.RemoteIpAddress
            }
        }, ct);

        var jwt = GenerateJwt(user);
        return AuthResult.Success(jwt);
    }
}

Data residency requirements are handled by deploying platform components in specific geographic regions. The platform supports multi-region deployment with data routing rules that ensure prompts and completions for European customers never leave EU-based infrastructure, while Asian customers' data stays within APAC regions.

22. Cost Estimation

Running an enterprise LLM platform is a significant financial commitment. Understanding the cost structure helps with capacity planning and pricing decisions.

Monthly Cost Breakdown (1,000 RPS steady state)

ComponentSpecificationMonthly Cost
GPU Compute (H100)1,560 GPUs × $2.50/hr$2,808,000
CPU Compute (K8s Nodes)200 nodes × 32 vCPU$192,000
PostgreSQL (Managed)Multi-AZ, 4TB storage$15,000
Redis Cluster12 nodes, 384GB RAM$8,000
Vector DatabaseWeaviate, 6-node cluster$12,000
Object Storage (S3)20TB stored, 100TB egress$5,000
Kafka / Event StreamingManaged, 30 partitions$6,000
Networking & CDNGlobal load balancing$10,000
Observability StackPrometheus, Grafana, Jaeger$5,000
Security & ComplianceWAF, DDoS, audit tools$8,000
Engineering Team (40 people)Avg $180K/yr salary$600,000
Total Monthly$3,069,000

Revenue Model

At 65 billion tokens per day with blended pricing of $2 per million tokens, daily revenue is approximately $130,000, or $3.9 million per month. This yields a gross margin of approximately 26%, which is typical for infrastructure-heavy SaaS businesses. The margin improves significantly as the platform scales, because GPU costs decrease per token with higher utilization.

Cost Optimization Strategies: Spot/preemptible GPUs can reduce compute costs by 60-70% for non-latency-critical workloads like fine-tuning. Auto-scaling with aggressive scale-down policies reduces costs during off-peak hours. Quantized models serve the same quality with fewer GPUs.

23. Testing the Platform

A rigorous testing strategy is essential for a platform that serves as critical infrastructure for hundreds of enterprise customers. We implement testing at multiple levels.

Testing Pyramid

LevelTypeToolsCoverage Target
UnitBusiness logic, token counting, billingxUnit, Moq85%
IntegrationDatabase, Redis, KafkaTestcontainersKey paths
ContractAPI compatibility with OpenAI specPactAll endpoints
E2EFull request flowPlaywright, k6Critical journeys
LoadPerformance and soak testingk6, Grafana CloudSLA targets
SafetyContent moderation accuracyCustom test suitePolicy coverage

Load Testing Script

JavaScript
// k6 load test for chat completions
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';

const errorRate = new Rate('errors');
const ttft = new Trend('time_to_first_token');

export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 500 },
    { duration: '10m', target: 1000 },
    { duration: '5m', target: 1000 },
    { duration: '2m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<2000', 'p(99)<5000'],
    errors: ['rate<0.01'],
  },
};

const API_KEY = __ENV.API_KEY;
const BASE_URL = __ENV.BASE_URL || 'https://api.platform.example.com';

export default function () {
  const payload = JSON.stringify({
    model: 'llama-3.1-70b',
    messages: [
      { role: 'system', content: 'You are a helpful assistant.' },
      { role: 'user', content: 'Explain quantum computing in simple terms.' }
    ],
    max_tokens: 256,
    stream: false
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,
    },
  };

  const res = http.post(`${BASE_URL}/v1/chat/completions`, payload, params);

  check(res, {
    'status is 200': (r) => r.status === 200,
    'has choices': (r) => {
      const body = JSON.parse(r.body);
      return body.choices && body.choices.length > 0;
    },
    'has usage': (r) => {
      const body = JSON.parse(r.body);
      return body.usage && body.usage.total_tokens > 0;
    },
    'latency acceptable': (r) => r.timings.duration < 5000,
  });

  errorRate.add(res.status !== 200);
  ttft.add(res.timings.connecting + res.timings.tls_handshaking);

  sleep(1);
}

Integration Test Example

C#
public class ChatCompletionsIntegrationTests : IClassFixture<TestContainersFixture>
{
    private readonly TestContainersFixture _fixture;
    private readonly HttpClient _client;

    public ChatCompletionsIntegrationTests(TestContainersFixture fixture)
    {
        _fixture = fixture;
        _client = fixture.CreateAuthenticatedClient("test-org-1");
    }

    [Fact]
    public async Task CreateChatCompletion_ReturnsValidResponse()
    {
        var request = new
        {
            model = "llama-3.1-8b",
            messages = new[]
            {
                new { role = "system", content = "You are helpful." },
                new { role = "user", content = "Hello, world!" }
            },
            max_tokens = 50
        };

        var response = await _client.PostAsJsonAsync(
            "/v1/chat/completions", request);

        response.EnsureSuccessStatusCode();
        var body = await response.Content.ReadFromJsonAsync<ChatCompletionResponse>();

        Assert.NotNull(body);
        Assert.Single(body.Choices);
        Assert.True(body.Usage.TotalTokens > 0);
        Assert.Equal("stop", body.Choices[0].FinishReason);
    }

    [Fact]
    public async Task CreateChatCompletion_QuotaExceeded_Returns429()
    {
        var fixture = _fixture.WithExhaustedQuota("test-org-quota-exhausted");
        var client = fixture.CreateAuthenticatedClient("test-org-quota-exhausted");

        var request = new
        {
            model = "llama-3.1-8b",
            messages = new[]
            {
                new { role = "user", content = "Test" }
            }
        };

        var response = await client.PostAsJsonAsync(
            "/v1/chat/completions", request);

        Assert.Equal(HttpStatusCode.TooManyRequests, response.StatusCode);
    }

    [Fact]
    public async Task CreateChatCompletion_SafetyBlocked_Returns400()
    {
        var request = new
        {
            model = "llama-3.1-8b",
            messages = new[]
            {
                new { role = "user", content = "How to create a bomb" }
            }
        };

        var response = await _client.PostAsJsonAsync(
            "/v1/chat/completions", request);

        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
        var body = await response.Content.ReadAsStringAsync();
        Assert.Contains("policy violation", body);
    }
}

24. Interview Q&A

Q1: How would you handle a sudden 10x spike in traffic to the LLM platform?

The platform must auto-scale at multiple levels. The Kubernetes Horizontal Pod Autoscaler (HPA) monitors GPU utilization and inference queue depth, spinning up new vLLM pods within 2-3 minutes. For the control plane, the API gateway and orchestration services scale on CPU and request count. To handle spikes faster than cold-start allows, we maintain warm standby capacity at 2x steady-state and use Kubernetes Cluster Autoscaler for node-level scaling. For extreme spikes, we implement request queuing with backpressure to gracefully degrade rather than fail outright.

Q2: How do you ensure consistent token counting across the platform when different models use different tokenizers?

Each model has an associated tokenizer configuration stored in the models table. The billing service loads the correct tokenizer for each request and counts tokens server-side before and after inference. We use the tiktoken library for GPT-family tokenizers and HuggingFace tokenizers for open models. The token counts are verified against the inference engine's own counts as a reconciliation step, and any discrepancy triggers an alert.

Q3: How would you design the RAG system to handle documents that are frequently updated?

Documents have a version number and a content hash. When a document is uploaded, we store both the raw file and its computed embeddings. When a document is updated, we compare the new content hash with the stored hash. If different, we re-chunk and re-embed the document, then atomically swap the vector entries using a transaction. The old embeddings are deleted after the new ones are confirmed. We use a background job queue to handle re-embedding asynchronously so uploads are fast. For very large documents, we implement incremental chunking that only re-processes chunks where the content actually changed.

Q4: How do you prevent one tenant from consuming all GPU resources and starving others?

We implement a multi-level fair scheduling approach. At the API gateway, each API key has a per-second rate limit. At the orchestration layer, each organization has a concurrency limit based on their plan tier. At the inference level, we use a weighted fair queuing scheduler that allocates GPU time proportionally to each tenant's allocation. Premium tenants can request dedicated GPU pools for guaranteed isolation. The system monitors per-tenant GPU consumption in real time and throttles tenants that exceed their allocation, returning 429 with appropriate headers.

Q5: Explain the trade-offs between streaming and non-streaming inference in an enterprise context.

Streaming provides a better user experience by showing tokens as they are generated, reducing perceived latency. However, it introduces complexity: billing must accumulate token counts across chunks, safety filtering must operate on partial output, and error handling is more complex since a stream can fail mid-response. For enterprise use cases, we recommend streaming for interactive chat and non-streaming for batch processing where latency is less critical and simpler error handling is preferred. Our platform supports both modes transparently, with the safety system performing output filtering on each chunk as it arrives.

Q6: How would you implement prompt injection detection?

Prompt injection is an adversarial attack where users craft inputs that override the system prompt. We use a layered defense: (1) structural validation that separates system and user message boundaries, (2) a fine-tuned classifier trained on known injection patterns, (3) output monitoring that detects when the model follows instructions inconsistent with its system prompt, and (4) canary tokens embedded in system prompts that trigger alerts if they appear in user input. No single technique is foolproof, so the combination provides defense in depth. We also maintain a continuously updated database of known attack patterns.

Q7: How do you handle model upgrades without breaking existing integrations?

We follow semantic versioning for models. When a new model version is available, it is deployed alongside the existing version. The routing system allows gradual traffic migration: first 5% of new tenants, then existing tenants opt-in, then automatic migration after a validation period. During this process, the old version remains available for at least 90 days. API response schemas are tested against a compatibility contract to ensure no breaking changes. We also provide a model alias system where llama-3.1-70b always points to the latest stable version of the 70B model, while llama-3.1-70b-v2.1 pins to a specific version.

Q8: Describe your approach to handling long-context requests that exceed a model's context window.

The platform handles this at multiple levels. First, the request validation layer checks if the total input tokens exceed the model's context window. If so, we offer several strategies: (1) automatic truncation with the most recent messages preserved, (2) sliding window summarization that compresses older messages, (3) routing to a model with a larger context window, and (4) for RAG requests, reducing the number of retrieved chunks. The client receives metadata about which strategy was applied so they can adjust their application logic accordingly. We also support models with up to 131K token context windows for use cases that genuinely need very long inputs.

Q9: How would you design the audit logging system for compliance with SOC 2 and HIPAA?

Audit logs are append-only and stored in a separate, isolated database with restricted access. Every API call, admin action, data access, and configuration change generates an audit entry with the actor identity, timestamp, action type, affected resources, and request metadata. Logs are encrypted at rest using AES-256 with keys managed by a separate KMS. Retention is configurable but defaults to 7 years for HIPAA compliance. The audit system writes asynchronously through Kafka to avoid impacting request latency, with a synchronous fallback to a local buffer that guarantees no entries are lost. Regular integrity checks verify that the log chain has not been tampered with.

Q10: How do you estimate the cost per token for a self-hosted model versus using a managed API?

The total cost of self-hosted inference includes: GPU amortized cost (purchase price divided by expected useful life), electricity and cooling, networking, engineering team time, and opportunity cost of capital. For a 70B model on H100 GPUs with 60% utilization, the cost is approximately $0.60-0.80 per million output tokens. For a managed API like OpenAI, the equivalent model costs $10-15 per million output tokens. However, the managed API requires zero infrastructure management and scales instantly. Self-hosting becomes cost-effective at approximately 50 billion tokens per month. Below that threshold, managed APIs are typically cheaper when you factor in engineering overhead.

Conclusion

Building an OpenAI-style enterprise LLM platform is one of the most complex infrastructure challenges in modern software engineering. It requires deep expertise in distributed systems, GPU infrastructure, machine learning operations, security, and compliance. The architecture we have designed in this guide addresses every major subsystem: from the API gateway that handles authentication and rate limiting, through the orchestration layer that coordinates RAG, safety, and prompt management, to the inference engine that serves models on GPU hardware with optimal performance.

The key architectural principles that make this system work are separation of the control plane from the data plane, multi-tenant isolation at every layer, event-driven billing and audit logging, and a model-agnostic serving infrastructure. By following these principles you can build a platform that starts with a single model and scales to hundreds of models serving thousands of enterprise customers.

As LLMs continue to evolve rapidly, the platform must be designed for change. The adapter pattern for inference backends, the pluggable safety pipeline, and the flexible routing rules all ensure that new models, new safety requirements, and new enterprise features can be added without disrupting existing customers. The investment in a well-architected platform pays dividends in operational reliability, customer trust, and the ability to iterate quickly in a fast-moving field.

Whether you are building this platform for internal use at a large enterprise or as a commercial product, the blueprint in this guide gives you a comprehensive starting point. Adapt the component choices to your specific constraints, start with the minimal viable platform, and iterate toward the full architecture as your scale and requirements grow.

© 2026 Ayodhyya. All rights reserved.

Design an OpenAI-Style Enterprise LLM Platform — The Complete Guide — A Senior+ Guide