DAY 22 WEEK 5: ADVANCED

AI & LLM Integration

Master multi-provider LLM support, dynamic model routing, budget enforcement, streaming responses, and cost tracking in Experience plugins.

AI features are table stakes for modern applications, but enterprise AI is different. You need cost controls so one user doesn't burn through the budget, policy compliance so sensitive data stays protected, and automatic failover when providers go down. EmpowerNow's LLM Proxy gives you all of this through a single, secure API.

📋Learning Objectives

  • Understand the LLM Provider Abstraction architecture
  • Configure and use multiple LLM providers (OpenAI, Anthropic, Azure, Ollama)
  • Learn how dynamic model routing selects cost-effective, policy-compliant models
  • Implement budget enforcement with soft and hard limits
  • Make streaming and non-streaming LLM requests from plugins
  • Track costs and understand pricing models

🏗️LLM Provider Architecture

The BFF provides a unified /api/llm/chat/completions endpoint that abstracts multiple providers with built-in authorization, budget controls, and failover.

flowchart TB subgraph CLIENTS["Plugins & Agents"] P1["Experience Plugin"] P2["AI Agent"] end subgraph BFF["BFF LLM Proxy"] EP["chat/completions"] ENF["Enforcement Layer"] ROUTE["Model Router"] BUDGET["Budget Manager
(Redis)"] end PDP["PDP
(Policy Check)"] subgraph PROVIDERS["LLM Providers"] OAI["OpenAI
gpt-4o-mini, gpt-4.1"] ANT["Anthropic
claude-3-5-sonnet"] AZ["Azure OpenAI
deployments"] OL["Ollama
llama3.2, mistral"] end P1 --> EP P2 --> EP EP --> ENF ENF --> PDP ENF --> BUDGET ENF --> ROUTE ROUTE --> OAI ROUTE --> ANT ROUTE --> AZ ROUTE --> OL style EP fill:#a855f722,stroke:#a855f7 style PDP fill:#f43f5e22,stroke:#f43f5e style BUDGET fill:#10b98122,stroke:#10b981

Key Features

🔀 Multi-Provider

OpenAI, Anthropic, Azure OpenAI, Ollama — all through one API

💰 Budget Control

Per-user/team spending limits with Redis-backed tracking

🔄 Auto-Failover

Circuit breakers detect failures, route to healthy providers

📊 Cost Tracking

Real-time token counting with per-model pricing

🛡️ Policy Enforcement

PDP controls which models users can access

🌊 Streaming

Server-sent events for real-time token delivery

🤖Supported Providers

Provider Models Input $/1M Output $/1M Features
OpenAI gpt-4o-mini, gpt-4.1, gpt-5-mini $0.50 - $5.00 $1.50 - $15.00 chat, stream, tools, vision, reasoning
Anthropic claude-3-5-sonnet, claude-3-haiku $0.25 - $3.00 $1.25 - $15.00 chat, stream, tools, vision
Azure OpenAI Same as OpenAI (deployments) Same as OpenAI Same as OpenAI chat, stream, tools
Ollama llama3.2, mistral, codellama $0.00 (local) $0.00 (local) chat, stream
💡 Cost Optimization

For development/testing, use Ollama (free, local) or gpt-4o-mini ($0.50/1M input). Reserve gpt-4.1 or claude-3-5-sonnet for production workloads requiring higher quality.

🔀Dynamic Model Routing

When a requested model is blocked by policy or over budget, the BFF automatically finds an alternative using a weighted score:

  • 70% Cost — cheaper models ranked higher
  • 20% Latency — faster response times preferred
  • 10% Error Rate — more reliable providers preferred
flowchart TD REQ["Request: model='gpt-4.1'"] --> CHK{"PDP allows
gpt-4.1?"} CHK -->|Yes| BUDG{"Within budget?"} CHK -->|No| TYPE{"Reason?"} BUDG -->|Yes| USE["✅ Use gpt-4.1"] BUDG -->|No| TYPE TYPE -->|Policy Only| DENY["❌ 403 Denied"] TYPE -->|Budget/Combined| CAND["Get alternatives"] CAND --> SORT["Sort by cost score"] SORT --> TRY["Try each until
one passes policy+budget"] TRY --> SEL["✅ Use gpt-4o-mini
(rerouted)"] style DENY fill:#f43f5e33,stroke:#f43f5e style USE fill:#10b98133,stroke:#10b981 style SEL fill:#f59e0b33,stroke:#f59e0b

Response Headers

Header Description Example
x-aria-model-selected The model actually used gpt-4o-mini
x-aria-model-rerouted Whether a different model was selected true
x-aria-budget-remaining Remaining budget in cents 1523
x-aria-decision-id PDP decision ID for audit dec-abc123

💰Budget Enforcement

Budget limits prevent runaway costs. The BFF tracks spending in Redis and checks PDP policies.

Budget Configuration

# PDP policy with budget constraints
rules:
  - effect: permit
    resource: "llm:openai:chat"
    action: "invoke"
    on_permit:
      constraints:
        model:
          allow: ["gpt-4o-mini", "gpt-4.1"]
        tokens:
          max_output: 2000
          max_stream: 4096
        spend_budget:
          scope: "user"          # user | team | org
          period: "monthly"      # daily | weekly | monthly
          limit_usd: 25.0        # Hard limit in USD

Budget Response Codes

Code Meaning Action
200 Success (within budget) Proceed normally
402 Budget exceeded Show upgrade prompt or wait for reset
403 Policy denied (model not allowed) Request different model or escalate
⚠️ Budget Holds

The BFF places a budget hold before calling the LLM (estimated cost × 1.2). After the response, actual cost is calculated and the difference is released. This prevents overspending during concurrent requests.

🔧Plugin SDK Integration

Non-Streaming Request

// src/hooks/useLlm.ts
const sdk = (globalThis as any).pluginSdk;

interface ChatMessage {
  role: 'system' | 'user' | 'assistant';
  content: string;
}

interface ChatOptions {
  model?: string;
  max_tokens?: number;
  temperature?: number;
}

export async function chat(
  messages: ChatMessage[],
  options: ChatOptions = {}
): Promise<string> {
  const response = await sdk.api.fetch('/api/llm/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: options.model || 'gpt-4o-mini',
      messages,
      max_tokens: options.max_tokens || 1000,
      temperature: options.temperature || 0.7,
      stream: false
    })
  });

  if (!response.ok) {
    if (response.status === 402) {
      throw new Error('Budget exceeded. Please contact your administrator.');
    }
    if (response.status === 403) {
      throw new Error('You do not have permission to use this model.');
    }
    throw new Error(`LLM request failed: ${response.status}`);
  }

  const data = await response.json();
  
  // Log rerouting info
  const rerouted = response.headers.get('x-aria-model-rerouted');
  const selected = response.headers.get('x-aria-model-selected');
  if (rerouted === 'true') {
    console.log(`Model rerouted to: ${selected}`);
  }

  return data.choices[0].message.content;
}

Streaming Request

// Streaming for real-time UI updates
export async function streamChat(
  messages: ChatMessage[],
  onToken: (token: string) => void,
  options: ChatOptions = {}
): Promise<void> {
  const response = await sdk.api.fetch('/api/llm/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: options.model || 'gpt-4o-mini',
      messages,
      max_tokens: options.max_tokens || 1000,
      stream: true
    })
  });

  if (!response.ok) {
    throw new Error(`LLM stream failed: ${response.status}`);
  }

  const reader = response.body?.getReader();
  const decoder = new TextDecoder();

  if (!reader) {
    throw new Error('No response body');
  }

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    const lines = chunk.split('\n').filter(line => line.startsWith('data: '));

    for (const line of lines) {
      const data = line.slice(6); // Remove 'data: ' prefix
      if (data === '[DONE]') return;

      try {
        const parsed = JSON.parse(data);
        const token = parsed.choices?.[0]?.delta?.content;
        if (token) {
          onToken(token);
        }
      } catch (e) {
        // Skip malformed chunks
      }
    }
  }
}

Usage in Component

// src/components/AiAssistant.tsx
const React = (globalThis as any).React;
const { useState } = React;

export function AiAssistant() {
  const [input, setInput] = useState('');
  const [response, setResponse] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async () => {
    setLoading(true);
    setResponse('');

    try {
      // Use streaming for better UX
      await streamChat(
        [
          { role: 'system', content: 'You are a helpful assistant.' },
          { role: 'user', content: input }
        ],
        (token) => setResponse(prev => prev + token)
      );
    } catch (err: any) {
      setResponse(`Error: ${err.message}`);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <textarea
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Ask me anything..."
      />
      <button onClick={handleSubmit} disabled={loading}>
        {loading ? 'Thinking...' : 'Send'}
      </button>
      <div className="response">{response}</div>
    </div>
  );
}
💡 plugins.yaml Permissions
permissions:
  api:
    - method: POST
      path: /api/llm/chat/completions

⚙️Provider Configuration

LLM providers are configured in config/providers.yaml. This file defines endpoints, credentials, pricing, circuit breakers, and fallback chains.

Complete Provider Schema

# config/providers.yaml
providers:
  - id: openai-primary              # Unique identifier
    kind: openai                    # Provider type: openai, anthropic, ollama
    name: "OpenAI Primary"          # Human-readable name
    enabled: true                   # Enable/disable this provider
    base_url: https://api.openai.com/v1
    api_key: ${OPENAI_API_KEY}      # Environment variable substitution
    default_model: gpt-4o-mini
    
    supported_features:             # Capabilities
      - chat
      - stream
      - tools
      - vision
      - reasoning
    
    egress:                         # Allowed outbound hosts
      allow:
        - api.openai.com
    
    pricing:                        # USD per token
      gpt-4o-mini:
        input: 0.0000005            # $0.50 per 1M tokens
        output: 0.0000015           # $1.50 per 1M tokens
      gpt-4.1:
        input: 0.000005
        output: 0.000015
    
    circuit_breaker:                # Failover control
      threshold: 5                  # Consecutive failures before opening
      timeout: 60                   # Seconds before half-open state
    
    timeouts:
      default: 30.0                 # Non-streaming timeout
      streaming: 120.0              # Streaming timeout

settings:
  health_check_interval: 30
  config_reload_interval: 300
  
  fallback_chains:                  # Automatic failover
    gpt:
      - openai-primary
      - anthropic-primary
      - ollama-local
    claude:
      - anthropic-primary
      - openai-primary
    default:
      - openai-primary
      - anthropic-primary

Circuit Breaker States

stateDiagram-v2 [*] --> Closed: Start Closed --> Open: threshold failures Open --> HalfOpen: timeout elapsed HalfOpen --> Closed: success HalfOpen --> Open: failure note right of Closed: Normal operation note right of Open: Provider skipped note right of HalfOpen: Testing recovery

Fallback Chain Behavior

# Request: model="gpt-4o-mini"
# Fallback chain: [openai-primary, anthropic-primary, ollama-local]

# Try openai-primary → Circuit open ❌
# Try anthropic-primary → Success ✅
# Response headers:
#   x-aria-provider: anthropic-primary
#   x-aria-model-rerouted: true

🛡️Classifier Guard

The Classifier Guard analyzes prompts before they reach the LLM, detecting and blocking risky content.

Blocked Categories

Label Description Min Confidence
prompt_injection Attempts to override system instructions 65%
jailbreak Attempts to bypass safety measures 75%
secrets Requests for credentials, API keys, passwords 60%
exfiltration Data extraction attempts 75%
malware Code generation for malicious purposes 75%

Per-Category Actions

# config/classifier.yaml
guard:
  enabled: true
  block_labels:
    - secrets
    - prompt_injection
    - jailbreak
  min_conf: 0.75
  per_label_thresholds:
    secrets: 0.60
    prompt_injection: 0.65

  actions:
    high_cost_request:
      cap_tokens: 2048
      route_model: gpt-4o-mini
      budget:
        hold_multiplier: 1.5
        max_cents: 5000
    
    tools_egress_required:
      disallow_stream: true
      cap_tokens: 1024
⚠️ Classification Latency

Classifier adds ~50-100ms to each request. For high-throughput, low-risk internal tools, consider disabling with CLASSIFIER_GUARD_ENABLED=false.

🔄Hot-Reload Configuration

Update provider configuration without restarting the BFF:

# 1. Edit the providers file
vim config/providers.yaml

# 2. Trigger reload (no restart needed)
curl -X POST http://bff:8000/api/admin/providers/reload \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# 3. Verify changes
curl http://bff:8000/health/providers | jq

What Happens on Reload

  • New providers are added instantly
  • Modified providers are replaced atomically
  • Removed providers are cleaned up
  • In-flight requests complete normally
  • Circuit breaker states are preserved
💡 Kubernetes ConfigMap Update

When using ConfigMaps, update the config and trigger reload:

kubectl edit configmap bff-providers-config
kubectl exec deployment/bff -- curl -X POST localhost:8000/api/admin/providers/reload

🔍Troubleshooting

402 Payment Required (Budget Exceeded)

Symptom: {"error": "Budget exceeded for scope user:xyz"}

Fixes:

  • Wait for budget reset (daily/weekly/monthly)
  • Request budget increase from admin
  • Use a cheaper model (e.g., gpt-4o-mini instead of gpt-4.1)

403 Forbidden (Model Not Allowed)

Symptom: {"error": "Model gpt-4.1 not allowed by policy"}

Debug:

# Check PDP policy directly
curl -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": { "type": "user", "id": "current" },
    "resource": { "type": "llm", "id": "openai:chat" },
    "action": { "name": "invoke" },
    "context": { 
      "model": "gpt-4.1",
      "reason_admin": true
    }
  }'

Response Rerouted Unexpectedly

Symptom: Requested gpt-4.1 but got gpt-4o-mini

Check:

  • Review x-aria-model-rerouted header
  • Check x-aria-budget-remaining — may be near limit
  • Check x-aria-decision-id and look up in receipts

Streaming Not Working

Symptom: Response comes all at once, not streamed

Fixes:

  • Ensure stream: true in request body
  • Don't use await response.json() — use response.body.getReader()
  • Some models/providers don't support streaming (check features)

Knowledge Check

1 What happens when a user requests an expensive model but is over budget?

The BFF attempts dynamic model routing: it finds cheaper alternatives (sorted by cost score) that the user is allowed to use and has budget for. If found, the request proceeds with the alternative model and x-aria-model-rerouted: true is returned. If no alternative is available, a 402 Payment Required is returned.

2 What's the difference between 402 and 403 errors?

402 Payment Required = Budget exceeded. The user is allowed to use the model but has run out of spending budget. 403 Forbidden = Policy denied. The user is not allowed to use the requested model at all, regardless of budget.

3 Why do budget "holds" exist?

Holds prevent overspending during concurrent requests. Before calling the LLM, the BFF reserves estimated_cost × 1.2 from the budget. After the response, actual cost is calculated and the difference is released. Without holds, 10 concurrent requests could each think they have budget and collectively exceed the limit.

4 How do you read streaming tokens in a plugin?
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  const chunk = decoder.decode(value, { stream: true });
  // Parse SSE "data: {...}" lines
}
5 Which header tells you if a different model was used than requested?

x-aria-model-rerouted — returns true if dynamic routing selected a different model. Check x-aria-model-selected to see which model was actually used.

📝Day 22 Checkpoint

  • Understand LLM Provider Abstraction architecture
  • Know the supported providers and their pricing
  • Understand how dynamic model routing works
  • Know the difference between 402 and 403 errors
  • Can make non-streaming LLM requests from plugins
  • Can make streaming LLM requests from plugins
  • Understand budget enforcement and holds