# AWS Bedrock LLM Proxy - Complete Implementation Plan **Date**: June 27, 2026 | **Status**: Ready for Development ## 📚 DOCUMENTATION & NAVIGATION - **📋 Index** → [README.md](README.md) - **⚡ Quick Reference** → [QUICK-REFERENCE.md](QUICK-REFERENCE.md) - **✅ Checklist** → [implementation-checklist.md](implementation-checklist.md) ## QUICK LINKS - **"What should I build?"** → [implementation-checklist.md](implementation-checklist.md) - **"How does spending work?"** → [Part 5: Spending Enforcement](#part-5-spending-enforcement) - **"Database schema?"** → [Part 6: Database Schema](#part-6-database-schema) - **"Testing guide?"** → [Part 8: Testing & QA](#part-8-testing--qa) --- ## 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` ```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 ```javascript // 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 ```javascript // 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) ```javascript 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 ```javascript 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 ```javascript // 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**: ```json { "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**: ```json { "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` ```javascript 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**: ```json { "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` ```javascript 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**: ```javascript // 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 ```javascript // 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 ```sql -- 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) - [x] Config loading from Docker secret - [x] OAuth discovery endpoint integration - [x] Dual auth middleware (JWT + Bearer) - [x] Single model (Claude Haiku) - [x] Basic spending tracking - [x] OpenAI `/v1/chat/completions` endpoint (non-streaming) **Deliverable**: ```bash 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) ```bash # 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