bedrock-proxy-implementation-plan.md 19 KB

AWS Bedrock LLM Proxy - Complete Implementation Plan

Date: June 27, 2026 | Status: Ready for Development

📚 DOCUMENTATION & NAVIGATION

QUICK LINKS


PART 1: ARCHITECTURE OVERVIEW

System Flow

Client Request (OpenAI v1 compatible)
    ↓
[Auth Middleware] → Detect Bearer token type (JWT vs opaque)
    ↓
[OAuth Validator] OR [Legacy Bearer Validator]
    ↓
[Spending Check] → Estimate cost, verify budget
    ↓
[Bedrock InvokeModel] → AWS API call, get tokens
    ↓
[Atomic Deduction] → PostgreSQL transaction
    ↓
[OpenAI-formatted Response] → Back to client

Key Architecture Decisions

  • Config: JSON file mounted as Docker secret at /run/secrets/bedrock-proxy-config.json
  • OAuth Discovery: Auto-fetch from https://data.titleproject.space/.well-known/oauth-authorization-server (cached 24h)
  • Dual Auth: Support both Bearer tokens (opaque + JWT) on same /v1/* endpoints
  • API: OpenAI v1 compatible (drop-in replacement for OpenAI clients)
  • Models: Native Bedrock model names (e.g., claude-3-haiku-20240307)

PART 2: CONFIGURATION SYSTEM

Docker Config File Structure

File: /run/secrets/bedrock-proxy-config.json

{
  "service": {
    "name": "bedrock-llm-proxy",
    "port": 3000,
    "log_level": "info"
  },
  "oauth": {
    "discovery_url": "https://data.titleproject.space/.well-known/oauth-authorization-server",
    "cache_ttl_hours": 24,
    "scopes_required": ["llm", "user"]
  },
  "bedrock": {
    "aws_region": "us-east-1",
    "aws_access_key_id": "AKIA...",
    "aws_secret_access_key": "..."
  },
  "database": {
    "url": "postgresql://user:pass@db:5432/bedrock_proxy",
    "pool_size": 20,
    "statement_timeout_ms": 5000
  },
  "spending": {
    "default_limit_usd": null,
    "monthly_reset_day": 1,
    "reset_hour_utc": 0
  }
}

Config Loading at Startup

// src/lib/config/loader.js
import fs from 'fs';

export async function loadConfig() {
  const configPath = process.env.CONFIG_PATH || '/run/secrets/bedrock-proxy-config.json';
  
  if (!fs.existsSync(configPath)) {
    throw new Error(`Config file not found: ${configPath}`);
  }

  const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  
  // Fetch OAuth discovery metadata (cached 24h)
  config.oauth.metadata = await fetchOAuthMetadata(config.oauth.discovery_url);
  
  return config;
}

async function fetchOAuthMetadata(discoveryUrl) {
  const response = await fetch(discoveryUrl);
  const metadata = await response.json();
  
  // Returns: { issuer, token_endpoint, introspection_endpoint, jwks_uri, ... }
  return metadata;
}

PART 3: DUAL AUTH MIDDLEWARE

Token Type Detection

// src/lib/auth/middleware.js

function isJwtToken(token) {
  // JWTs have exactly 3 base64url parts separated by dots
  const parts = token.split('.');
  return parts.length === 3 && parts[0] && parts[1] && parts[2];
}

export async function authMiddleware(request, config) {
  const authHeader = request.headers.get('authorization');
  
  if (!authHeader) {
    throw new Error('Missing Authorization header');
  }

  const [scheme, token] = authHeader.split(' ');
  if (scheme.toLowerCase() !== 'bearer') {
    throw new Error('Only Bearer authentication supported');
  }

  // Route to appropriate validator
  if (isJwtToken(token)) {
    return await validateOAuthToken(token, config);
  } else {
    return await validateLegacyBearerToken(token, config);
  }
}

OAuth Token Validation (JWT)

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

async function validateOAuthToken(token, config) {
  try {
    const decoded = jwt.decode(token, { complete: true });
    if (!decoded) throw new Error('Invalid JWT format');
    
    // Fetch signing key from JWKS
    const client = jwksClient({ jwksUri: config.oauth.metadata.jwks_uri });
    const key = await client.getSigningKey(decoded.header.kid);
    
    // Verify signature
    const claims = jwt.verify(token, key.getPublicKey(), {
      algorithms: ['RS256'],
      issuer: config.oauth.metadata.issuer
    });

    // Validate scope
    const tokenScopes = (claims.scope || '').split(' ');
    const requiredScopes = config.oauth.scopes_required; // ["llm", "user"]
    
    if (!requiredScopes.every(s => tokenScopes.includes(s))) {
      throw new Error(`Insufficient scope. Required: ${requiredScopes.join(', ')}`);
    }

    return {
      auth_type: 'oauth',
      user_id: claims.sub,
      email: claims.email,
      scope: claims.scope,
      expires_at: new Date(claims.exp * 1000)
    };
  } catch (error) {
    throw new Error(`OAuth validation failed: ${error.message}`);
  }
}

Legacy Bearer Token Validation

async function validateLegacyBearerToken(token, config) {
  try {
    // Use OAuth introspection endpoint to validate opaque token
    const introspectionUrl = config.oauth.metadata.introspection_endpoint;
    
    const response = await fetch(introspectionUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: `token=${encodeURIComponent(token)}`
    });

    if (!response.ok) throw new Error('Token introspection failed');
    
    const data = await response.json();
    
    if (!data.active) {
      throw new Error('Token inactive or expired');
    }

    return {
      auth_type: 'bearer',
      user_id: data.sub,
      email: data.username,
      scope: data.scope,
      expires_at: new Date(data.exp * 1000)
    };
  } catch (error) {
    throw new Error(`Bearer token validation failed: ${error.message}`);
  }
}

SvelteKit Hook Integration

// src/hooks.server.js
import { authMiddleware } from '$lib/auth/middleware.js';
import { loadConfig } from '$lib/config/loader.js';

let config;

export async function init() {
  config = await loadConfig();
}

export async function handle({ event, resolve }) {
  // Skip auth for public endpoints
  if (!event.url.pathname.startsWith('/api/v1/')) {
    return resolve(event);
  }

  try {
    event.locals.user = await authMiddleware(event.request, config);
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: {
          code: 'authentication_error',
          message: error.message,
          type: 'authentication_error'
        }
      }),
      { status: 401, headers: { 'Content-Type': 'application/json' } }
    );
  }

  return resolve(event);
}

PART 4: OPENAI v1 API IMPLEMENTATION

Endpoint: POST /v1/chat/completions

Request:

{
  "model": "claude-3-haiku-20240307",
  "messages": [
    {"role": "system", "content": "You are helpful."},
    {"role": "user", "content": "Hello"}
  ],
  "temperature": 0.7,
  "max_tokens": 1024,
  "stream": false
}

Response:

{
  "id": "chatcmpl-123abc",
  "object": "chat.completion",
  "created": 1704067200,
  "model": "claude-3-haiku-20240307",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello! How can I help?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 10,
    "completion_tokens": 8,
    "total_tokens": 18
  }
}

Implementation: src/routes/api/v1/chat/completions/+server.js

import { json } from '@sveltejs/kit';
import { invokeBedrockModel } from '$lib/bedrock/client.js';
import { checkSpendingLimit, deductSpending } from '$lib/spending/enforcer.js';
import { estimateCost } from '$lib/bedrock/models.js';

export async function POST({ request, locals }) {
  const payload = await request.json();
  const { model, messages, temperature, max_tokens, stream } = payload;

  // Validation
  if (!model || !messages) {
    return json({
      error: {
        code: 'invalid_request_error',
        message: 'model and messages are required'
      }
    }, { status: 400 });
  }

  const userId = locals.user.user_id;

  // Pre-flight spending check
  const costEstimate = estimateCost(model, messages, max_tokens);
  const spendingCheck = await checkSpendingLimit(userId, model, messages);
  
  if (!spendingCheck.allowed) {
    return json({
      error: {
        code: 'insufficient_credits',
        message: `Budget exceeded. Remaining: $${spendingCheck.remaining}`
      }
    }, { status: 429 });
  }

  try {
    // Call Bedrock
    const response = await invokeBedrockModel(model, messages, {
      temperature: temperature || 0.7,
      max_tokens: max_tokens || 1024
    });

    // Deduct actual cost
    await deductSpending(userId, model, response.input_tokens, response.output_tokens);

    // Format OpenAI response
    return json({
      id: `bedrock-${Date.now()}`,
      object: 'chat.completion',
      created: Math.floor(Date.now() / 1000),
      model,
      choices: [{
        index: 0,
        message: {
          role: 'assistant',
          content: response.completion
        },
        finish_reason: 'stop'
      }],
      usage: {
        prompt_tokens: response.input_tokens,
        completion_tokens: response.output_tokens,
        total_tokens: response.input_tokens + response.output_tokens
      }
    });
  } catch (error) {
    if (error.message.includes('Model not found')) {
      return json({
        error: {
          code: 'model_not_found_error',
          message: `Model not supported: ${model}`
        }
      }, { status: 404 });
    }
    
    return json({
      error: {
        code: 'server_error',
        message: error.message,
        type: 'server_error'
      }
    }, { status: 500 });
  }
}

Endpoint: GET /v1/models

Response:

{
  "object": "list",
  "data": [
    {
      "id": "claude-3-haiku-20240307",
      "object": "model",
      "created": 1704067200,
      "owned_by": "anthropic",
      "permission": [],
      "root": "claude-3-haiku-20240307",
      "parent": null
    }
  ]
}

Implementation: src/routes/api/v1/models/+server.js

import { json } from '@sveltejs/kit';
import postgres from 'postgres';

const sql = postgres(process.env.DATABASE_URL);

export async function GET({ locals }) {
  const models = await sql`
    SELECT model_id, provider, created_at FROM bedrock_models WHERE supported = true
  `;

  return json({
    object: 'list',
    data: models.map(m => ({
      id: m.model_id,
      object: 'model',
      created: Math.floor(new Date(m.created_at).getTime() / 1000),
      owned_by: m.provider,
      permission: [],
      root: m.model_id,
      parent: null
    }))
  });
}

Streaming Endpoint: POST /v1/chat/completions (stream: true)

Response (Server-Sent Events):

event: delta
data: {"choices":[{"index":0,"delta":{"content":"Hello"}}]}

event: delta
data: {"choices":[{"index":0,"delta":{"content":" there"}}]}

event: delta
data: {"choices":[{"index":0,"finish_reason":"stop"}]}

Implementation:

// Add to POST handler above:

if (stream) {
  return new Response(streamBedrockResponse(model, messages, userId), {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive'
    }
  });
}

async function* streamBedrockResponse(model, messages, userId) {
  try {
    const response = await invokeBedrockModelStreaming(model, messages);
    
    for await (const delta of response.deltas) {
      yield `event: delta\ndata: ${JSON.stringify({
        choices: [{ index: 0, delta: { content: delta.token } }]
      })}\n\n`;
    }

    // Final message with finish_reason
    yield `event: delta\ndata: ${JSON.stringify({
      choices: [{ index: 0, finish_reason: 'stop' }]
    })}\n\n`;

    // Deduct actual cost after streaming completes
    await deductSpending(userId, model, response.input_tokens, response.output_tokens);
  } catch (error) {
    yield `event: error\ndata: ${JSON.stringify({
      error: {
        code: 'server_error',
        message: error.message
      }
    })}\n\n`;
  }
}

PART 5: SPENDING ENFORCEMENT

Pre-Flight Check

// src/lib/spending/enforcer.js
import postgres from 'postgres';

export async function checkSpendingLimit(userId, modelId, messages) {
  const sql = postgres(process.env.DATABASE_URL);
  
  // Get user's current balance
  const [user] = await sql`
    SELECT user_id, current_spend_usd, monthly_limit_usd, spend_reset_at
    FROM users
    WHERE user_id = ${userId}
  `;

  if (!user) {
    throw new Error('User not found');
  }

  // Check if spending period has reset
  if (new Date() > user.spend_reset_at) {
    await sql`
      UPDATE users
      SET current_spend_usd = 0, spend_reset_at = NOW() + INTERVAL '1 month'
      WHERE user_id = ${userId}
    `;
    user.current_spend_usd = 0;
  }

  // If no limit set, allow unlimited
  if (user.monthly_limit_usd === null) {
    return { allowed: true, reason: 'unlimited' };
  }

  // Estimate tokens for this request
  const messageText = messages.map(m => m.content).join(' ');
  const estimatedTokens = Math.ceil(messageText.length / 4) * 1.2; // 20% buffer
  const estimatedCost = (estimatedTokens * 0.001); // Rough estimate

  const remainingBudget = user.monthly_limit_usd - user.current_spend_usd;
  
  if (estimatedCost > remainingBudget) {
    return {
      allowed: false,
      remaining: remainingBudget,
      estimated: estimatedCost
    };
  }

  return { allowed: true, estimated_cost: estimatedCost };
}

export async function deductSpending(userId, modelId, inputTokens, outputTokens) {
  const sql = postgres(process.env.DATABASE_URL);
  
  // Get model pricing
  const [model] = await sql`
    SELECT input_price_usd, output_price_usd FROM bedrock_models WHERE model_id = ${modelId}
  `;

  if (!model) throw new Error(`Model ${modelId} not found`);

  const actualCost = (
    (inputTokens * model.input_price_usd + outputTokens * model.output_price_usd) / 1_000_000
  );

  // Atomic deduction
  await sql`
    BEGIN;
    
    -- Lock row to prevent race conditions
    SELECT user_id FROM users WHERE user_id = ${userId} FOR UPDATE;
    
    -- Deduct cost
    UPDATE users
    SET current_spend_usd = current_spend_usd + ${actualCost}
    WHERE user_id = ${userId};

    -- Log transaction
    INSERT INTO spending_records (user_id, model_id, input_tokens, output_tokens, cost_usd)
    VALUES (${userId}, ${modelId}, ${inputTokens}, ${outputTokens}, ${actualCost});
    
    COMMIT;
  `;

  return { cost_deducted: actualCost };
}

PART 6: DATABASE SCHEMA

-- Users table
CREATE TABLE users (
  user_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  oauth_sub VARCHAR(255) UNIQUE NOT NULL,
  email VARCHAR(255) UNIQUE NOT NULL,
  monthly_limit_usd DECIMAL(10, 2) DEFAULT NULL,
  current_spend_usd DECIMAL(10, 2) DEFAULT 0,
  spend_reset_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + INTERVAL '1 month',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Spending records
CREATE TABLE spending_records (
  record_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(user_id),
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  model_id VARCHAR(64) NOT NULL,
  input_tokens INT NOT NULL,
  output_tokens INT NOT NULL,
  cost_usd DECIMAL(10, 4) NOT NULL
);

-- Bedrock models
CREATE TABLE bedrock_models (
  model_id VARCHAR(64) PRIMARY KEY,
  provider VARCHAR(32) NOT NULL,
  input_price_usd DECIMAL(10, 6) NOT NULL,
  output_price_usd DECIMAL(10, 6) NOT NULL,
  supported BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Indexes
CREATE INDEX idx_users_oauth_sub ON users(oauth_sub);
CREATE INDEX idx_spending_user_timestamp ON spending_records(user_id, timestamp DESC);

-- Monthly reset (cron: runs daily at 00:00 UTC)
CREATE OR REPLACE FUNCTION reset_monthly_spend()
RETURNS void AS $$
BEGIN
  UPDATE users
  SET current_spend_usd = 0, spend_reset_at = NOW() + INTERVAL '1 month'
  WHERE spend_reset_at <= NOW();
END;
$$ LANGUAGE plpgsql;

PART 7: IMPLEMENTATION PHASES

Phase 1: MVP (Days 1-3)

  • Config loading from Docker secret
  • OAuth discovery endpoint integration
  • Dual auth middleware (JWT + Bearer)
  • Single model (Claude Haiku)
  • Basic spending tracking
  • OpenAI /v1/chat/completions endpoint (non-streaming)

Deliverable:

curl -X POST http://localhost:3000/api/v1/chat/completions \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-3-haiku-20240307",
    "messages": [{"role": "user", "content": "Hello"}]
  }' | jq .

Phase 2: Multi-Model + Dashboard (Days 4-6)

  • All Bedrock models support
  • /v1/models endpoint
  • Streaming support (stream: true)
  • User spend dashboard
  • Admin analytics

Phase 3: Admin Controls (Days 7-10, Optional)

  • Admin UI to adjust user limits
  • Email notifications at 50%/80%/100%
  • Spending reports (CSV export)

PART 8: TESTING & QA

Phase 1 QA (Curl-based)

# Test 1: Valid OAuth token
TOKEN=$(curl -s https://title-graphql.example.com/oauth/token | jq -r .access_token)
curl -X POST http://localhost:3000/api/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"model":"claude-3-haiku-20240307","messages":[{"role":"user","content":"Hi"}]}'

# Test 2: Invalid token
curl -X POST http://localhost:3000/api/v1/chat/completions \
  -H "Authorization: Bearer invalid_token" \
  -d '{"model":"claude-3-haiku-20240307","messages":[{"role":"user","content":"Hi"}]}'
# Expected: 401 authentication_error

# Test 3: Spending limit
curl -X POST http://localhost:3000/api/v1/chat/completions \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"model":"claude-3-haiku-20240307","messages":[{"role":"user","content":"'$(printf 'a%.0s' {1..10000})'"}]}'
# Expected: 429 insufficient_credits (if limit exceeded)

NEXT STEPS

  1. Verify OAuth discovery works: curl https://data.titleproject.space/.well-known/oauth-authorization-server
  2. Setup PostgreSQL: Run schema DDL
  3. Create AWS IAM user: With bedrock:InvokeModel permission
  4. Mount config file: /run/secrets/bedrock-proxy-config.json
  5. Start Phase 1 implementation: Begin with auth middleware