Proxy Pattern
Provide a surrogate or placeholder for another object to control access to it.
The Problemβ
You're calling an LLM API from your app. Every call costs money and takes time. The same question asked twice shouldn't cost twice. Users spam the button. And when the primary provider goes down, your app crashes.
The naive approach:
async function askLLM(question: string): Promise<string> {
const response = await fetch("https://api.openai.com/v1/chat", {
method: "POST",
body: JSON.stringify({ prompt: question }),
});
return response.json(); // $0.003 every single time
}
// "What is the capital of France?" asked 10 times = 10 API calls = $0.03
// Provider goes down? App crashes.
// User spams? You burn through your rate limit.
No cache. No rate limit. No fallback. Every call hits the API, burns money, and prays the provider stays up.
The Solutionβ
Wrap the function with proxy layers. Same interface, three layers of protection:
import { memoize, throttle } from "@pithos/core/arkhe";
import { withFallback } from "@pithos/core/eidos/strategy/strategy";
// 1. Cache: same question = instant response, $0.000
const cachedAsk = memoize(askLLM);
// 2. Rate limit: max 1 call per second
const rateLimitedAsk = throttle(cachedAsk, 1000);
// 3. Fallback: if primary fails, try backup silently
const resilientAsk = withFallback(rateLimitedAsk, askBackupLLM);
// Consumer code is identical β just a function call
const answer = await resilientAsk("What is the capital of France?");
// First call: 1.2s, $0.003 β cache miss
// Second call: 2ms, $0.000 β cache hit β‘
Three Pithos utilities, three proxy layers. The consumer never knows about caching, rate limiting, or failover.
Live Demoβ
Ask questions to a simulated LLM and watch the proxy in action: cache hits, rate limits, and provider failover.
- llmProxy.ts
- Usage
/**
* LLM Proxy demo: memoize from Pithos for caching,
* manual rate-limit, and fallback provider.
*/
import { memoize } from "@pithos/core/eidos/proxy/proxy";
import type { ProxyLogEntry, ProxyStats } from "./types";
import { RESPONSES, DEFAULT_RESPONSE, PRIMARY_COST, FALLBACK_COST, RATE_LIMIT_WINDOW } from "./data";
// Re-export for consumers
export type { ProxyLogEntry, ProxyStats } from "./types";
export { PRESET_QUESTIONS } from "./data";
async function simulateCall(question: string, delayMs: number): Promise<string> {
await new Promise((r) => setTimeout(r, delayMs));
return RESPONSES[question] ?? DEFAULT_RESPONSE;
}
export function createLLMProxy() {
let simulateFailure = false;
let lastCallTime = 0;
let logId = 0;
const cachedPrimaryCall = memoize(
(question: string) => simulateCall(question, 800 + Math.random() * 600),
);
const cachedBackupCall = memoize(
(question: string) => simulateCall(question, 1200 + Math.random() * 800),
);
const stats: ProxyStats = {
logs: [],
totalCost: 0,
totalSaved: 0,
cacheHits: 0,
rateLimitHits: 0,
fallbackHits: 0,
};
function addEntry(entry: ProxyLogEntry) {
stats.logs = [entry, ...stats.logs];
}
async function ask(question: string): Promise<{ entry: ProxyLogEntry; stats: ProxyStats }> {
const now = Date.now();
const elapsed = now - lastCallTime;
// Rate limit
if (lastCallTime > 0 && elapsed < RATE_LIMIT_WINDOW) {
const entry: ProxyLogEntry = {
id: ++logId, question, type: "rate-limited", provider: null,
duration: 0, cost: 0,
response: `Rate limited. Try again in ${Math.ceil((RATE_LIMIT_WINDOW - elapsed) / 1000)}s.`,
timestamp: now,
};
stats.rateLimitHits++;
addEntry(entry);
return { entry, stats: { ...stats } };
}
lastCallTime = now;
const start = performance.now();
// Primary or fallback
const useFallback = simulateFailure;
const call = useFallback ? cachedBackupCall : cachedPrimaryCall;
const cost = useFallback ? FALLBACK_COST : PRIMARY_COST;
const response = await call(question);
const duration = Math.round(performance.now() - start);
const isCacheHit = duration < 50;
const entry: ProxyLogEntry = {
id: ++logId, question,
type: isCacheHit ? "cache-hit" : useFallback ? "fallback" : "cache-miss",
provider: isCacheHit ? null : useFallback ? "backup" : "primary",
duration, cost: isCacheHit ? 0 : cost, response, timestamp: now,
};
if (isCacheHit) {
stats.cacheHits++;
stats.totalSaved += cost;
} else {
stats.totalCost += cost;
if (useFallback) stats.fallbackHits++;
}
addEntry(entry);
return { entry, stats: { ...stats } };
}
return {
ask,
setSimulateFailure: (v: boolean) => { simulateFailure = v; },
getSimulateFailure: () => simulateFailure,
reset: () => {
Object.assign(stats, { logs: [], totalCost: 0, totalSaved: 0, cacheHits: 0, rateLimitHits: 0, fallbackHits: 0 });
lastCallTime = 0;
logId = 0;
},
stats,
};
}
import { useState } from "react";
import { RotateCcw, Send, Zap, AlertTriangle, Terminal, X } from "lucide-react";
import { PRESET_QUESTIONS } from "@/lib/llmProxy";
import { useLLMProxy } from "@/hooks/useLLMProxy";
import { StatsPanel } from "./StatsPanel";
import { TinyLLMLogo } from "./TinyLLMLogo";
import { ProxyLog } from "./ProxyLog";
export function LLMProxy() {
const {
stats, input, setInput, loading, lastResponse, simulateFailure, inputRef,
handleAsk, handleReset, toggleFailure, handleSpam,
} = useLLMProxy();
const [logOpen, setLogOpen] = useState(false);
return (
<div className="h-screen flex flex-col bg-[#09090f] text-white overflow-hidden relative">
{/* Glow effects */}
<div className="absolute top-0 left-1/4 w-96 h-96 bg-violet-600/10 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-indigo-600/8 rounded-full blur-3xl pointer-events-none" />
{/* Header */}
<div className="shrink-0 border-b border-white/[0.06] relative z-10">
<div className="max-w-5xl mx-auto px-3 sm:px-4 h-12 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<TinyLLMLogo size={18} />
<span className="text-sm font-bold tracking-tight bg-gradient-to-r from-violet-300 to-indigo-300 bg-clip-text text-transparent">TinyLLM</span>
</div>
<div className="hidden sm:flex items-center gap-2 ml-3 text-[10px] text-white/25">
<span>Proxy pattern</span>
<span>Β·</span>
<span>cache, rate-limit, fallback</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setLogOpen(!logOpen)}
className="md:hidden p-1.5 rounded-md bg-white/[0.04] border border-white/[0.06] relative"
>
{logOpen ? <X className="w-3.5 h-3.5 text-white/50" /> : <Terminal className="w-3.5 h-3.5 text-violet-400" />}
{!logOpen && stats.logs.length > 0 && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-violet-500 rounded-full text-[8px] font-bold text-white flex items-center justify-center">
{stats.logs.length}
</span>
)}
</button>
<button onClick={handleReset} className="p-1.5 rounded-md text-white/30 hover:text-white/60 hover:bg-white/[0.04] transition-colors" title="Reset">
<RotateCcw className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
{/* Content */}
<div className="flex-1 min-h-0 overflow-auto md:overflow-hidden relative z-10">
{/* Mobile: log overlay */}
{logOpen && (
<div className="md:hidden h-full p-3 flex flex-col">
<ProxyLog logs={stats.logs} className="flex-1" />
</div>
)}
<div className={`max-w-5xl mx-auto px-3 sm:px-4 py-4 h-full ${logOpen ? "hidden md:block" : ""}`}>
<div className="grid md:grid-cols-2 gap-4 h-full">
{/* Left column */}
<div className="space-y-3">
{/* Controls */}
<div className="flex gap-2">
<button
onClick={toggleFailure}
className={`flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-all flex items-center justify-center gap-1.5 border ${
simulateFailure
? "bg-red-500/10 text-red-400 border-red-500/20 hover:bg-red-500/20"
: "bg-white/[0.03] text-white/40 border-white/[0.06] hover:bg-white/[0.06]"
}`}
>
<AlertTriangle className="w-3 h-3" />
{simulateFailure ? "Failure ON" : "Simulate failure"}
</button>
<button
onClick={handleSpam}
disabled={loading}
className="px-3 py-2 bg-amber-500/10 text-amber-400 border border-amber-500/20 hover:bg-amber-500/20 rounded-lg text-xs font-medium transition-all flex items-center gap-1.5 disabled:opacity-30"
>
<Zap className="w-3 h-3" /> Γ5
</button>
</div>
<div className="h-px bg-white/[0.06]" />
{/* Quick questions */}
<div className="space-y-1.5">
<div className="text-[10px] text-white/25 font-medium uppercase tracking-wider">Quick questions</div>
{PRESET_QUESTIONS.map((q) => (
<button
key={q}
onClick={() => handleAsk(q)}
disabled={loading}
className="w-full text-left px-3 py-2 text-xs bg-white/[0.03] hover:bg-white/[0.06] border border-white/[0.04] rounded-lg transition-colors disabled:opacity-30 truncate text-white/50"
>
{q}
</button>
))}
</div>
{/* Ask input */}
<form onSubmit={(e) => { e.preventDefault(); handleAsk(input); }} className="flex gap-2">
<input
ref={inputRef}
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Or type your own question..."
className="flex-1 px-3 py-2.5 bg-white/[0.04] border border-white/[0.08] rounded-xl text-sm text-white placeholder-white/20 focus:outline-none focus:ring-2 focus:ring-violet-500/30 focus:border-violet-500/30 transition-all"
disabled={loading}
/>
<button
type="submit"
disabled={loading || !input.trim()}
className="px-4 py-2.5 bg-gradient-to-r from-violet-600 to-indigo-600 text-white rounded-xl text-sm font-medium hover:from-violet-500 hover:to-indigo-500 disabled:opacity-30 disabled:cursor-not-allowed transition-all shadow-lg shadow-violet-600/20 flex items-center gap-1.5"
>
<Send className="w-3.5 h-3.5" />
<span className="hidden sm:inline">Ask</span>
</button>
</form>
{/* Response */}
{(loading || lastResponse) && (
<div className="bg-white/[0.03] rounded-xl border border-white/[0.06] p-3">
{loading ? (
<div className="flex items-center gap-2">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-violet-500 border-t-transparent" />
<span className="text-xs text-white/30">Thinking...</span>
</div>
) : (
<p className="text-sm text-white/70 leading-relaxed">{lastResponse}</p>
)}
</div>
)}
<div className="h-px bg-white/[0.06]" />
{/* Status + Stats */}
<div className="flex gap-2">
<div className={`flex-1 px-2.5 py-2 rounded-lg border text-[10px] transition-all ${
simulateFailure
? "bg-red-500/10 border-red-500/20 text-red-400"
: "bg-emerald-500/10 border-emerald-500/20 text-emerald-400"
}`}>
<span className="font-medium">Primary</span> Β· {simulateFailure ? "β Down" : "β Online"}
</div>
<div className={`flex-1 px-2.5 py-2 rounded-lg border text-[10px] transition-all ${
simulateFailure
? "bg-amber-500/10 border-amber-500/20 text-amber-400"
: "bg-white/[0.03] border-white/[0.06] text-white/25"
}`}>
<span className="font-medium">Backup</span> Β· {simulateFailure ? "β‘ Active" : "Standby"}
</div>
</div>
<StatsPanel stats={stats} />
</div>
{/* Right: Proxy Log (desktop only) */}
<div className="hidden md:flex md:flex-col md:min-h-0">
<ProxyLog logs={stats.logs} className="flex-1 min-h-0" />
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="shrink-0 border-t border-white/[0.04] py-1.5 text-center text-[10px] text-white/15 relative z-10">
<code className="text-white/20">memoize()</code> Β· Proxy pattern Β· cache, rate-limit, fallback
</div>
</div>
);
}
Real-World Analogyβ
A credit card is a proxy for your bank account. It provides the same "pay for things" interface, but adds access control (credit limit), logging (transaction history), and can work offline (signature-based transactions).
When to Use Itβ
- Cache expensive computations or API calls
- Rate-limit access to external services
- Add fallback/retry logic without changing consumer code
- Add logging or metrics transparently
- Lazy-load resources on first access
When NOT to Use Itβ
If the function is cheap, fast, and reliable, a proxy adds overhead for no benefit. Don't cache a function that returns Date.now() or wrap a pure synchronous calculation in a rate limiter.
Proxy Variants in Arkheβ
Arkhe provides several proxy utilities:
import { memoize, once, throttle, debounce, lazy, guarded } from "@pithos/core/arkhe";
// Caching proxy β cache results by arguments
const cached = memoize(expensiveCalculation);
// Single-execution proxy β run only once
const initialize = once(loadConfig);
// Rate-limiting proxies
const throttled = throttle(saveToServer, 1000); // max once per second
const debounced = debounce(search, 300); // wait for pause in calls
// Lazy initialization proxy
const config = lazy(() => loadExpensiveConfig());
// Conditional execution proxy
const adminOnly = guarded(deleteUser, (user) => user.isAdmin);
APIβ
These functions are from Arkhe and re-exported by Eidos: