Day 19 Week 3 — Week 3

Cost Control: Monitoring and Optimizing LLM Spend

Day 19 — Cost Control: Monitoring and Optimizing LLM Spend

LLM costs scale linearly with usage — there's no volume discount on tokens. Without cost controls, a popular feature or a bug in a retry loop can burn through your budget in hours. Cost management isn't an afterthought; it's a core architectural concern.

1. Cost Attribution: Per-User, Per-Feature, Per-Pipeline

You can't optimize what you don't measure. Track cost at the granularity that lets you make decisions:

interface CostEvent {
  timestamp: Date;
  traceId: string;
  model: string;
  inputTokens: number;
  outputTokens: number;
  cost: number;
  // Attribution dimensions
  userId?: string;
  feature: string;       // 'classification', 'chat', 'summarization'
  pipeline?: string;     // 'document-analysis-v2'
  step?: string;         // 'extract', 'analyze'
  cached: boolean;
}

class CostTracker {
  private events: CostEvent[] = [];
  private dailyBudget: number;
  private alertThreshold: number;

  constructor(dailyBudget: number, alertThreshold = 0.8) {
    this.dailyBudget = dailyBudget;
    this.alertThreshold = alertThreshold;
  }

  record(event: CostEvent): void {
    this.events.push(event);
    this.checkBudget();
  }

  private checkBudget(): void {
    const todayCost = this.getTodayCost();
    if (todayCost > this.dailyBudget * this.alertThreshold) {
      this.alert(`Daily cost ${todayCost.toFixed(2)} approaching budget ${this.dailyBudget}`);
    }
    if (todayCost > this.dailyBudget) {
      this.alert(`BUDGET EXCEEDED: ${todayCost.toFixed(2)} > ${this.dailyBudget}`);
    }
  }

  // Analytics
  costByFeature(days = 7): Record<string, number> {
    const cutoff = Date.now() - days * 86400_000;
    return this.events
      .filter(e => e.timestamp.getTime() > cutoff)
      .reduce((acc, e) => {
        acc[e.feature] = (acc[e.feature] || 0) + e.cost;
        return acc;
      }, {} as Record<string, number>);
  }

  costByUser(days = 7): { userId: string; cost: number }[] {
    const cutoff = Date.now() - days * 86400_000;
    const byUser = new Map<string, number>();
    for (const e of this.events.filter(e => e.timestamp.getTime() > cutoff)) {
      if (e.userId) {
        byUser.set(e.userId, (byUser.get(e.userId) || 0) + e.cost);
      }
    }
    return [...byUser.entries()]
      .map(([userId, cost]) => ({ userId, cost }))
      .sort((a, b) => b.cost - a.cost);
  }
}

2. Token Optimization Techniques

// 1. Prompt compression — remove redundant instructions
const verbosePrompt = `
Please carefully analyze the following text. I would like you to identify
the main topics discussed. Please be thorough in your analysis and make sure
to consider all aspects. Format your response as JSON.
`; // ~40 tokens

const optimizedPrompt = `
Identify main topics. Return JSON array of strings.
`; // ~12 tokens — 70% savings, same output quality

// 2. Output length control
const response = await llm.complete({
  messages: [{ role: 'user', content: prompt }],
  max_tokens: 200,  // cap output — output tokens are 3-5x more expensive
});

// 3. System prompt caching (Anthropic)
const cachedResponse = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  system: [{
    type: 'text',
    text: longSystemPrompt,        // 2000 tokens, cached
    cache_control: { type: 'ephemeral' },
  }],
  messages: [{ role: 'user', content: shortQuery }],
});
// Cached tokens: 10% of normal cost after first call

// 4. Batch similar requests into one call
async function batchClassify(items: string[]): Promise<string[]> {
  // One call for 20 items instead of 20 separate calls
  const result = await llm.complete({
    messages: [{
      role: 'user',
      content: `Classify each:\n${items.map((item, i) => `${i+1}. ${item}`).join('\n')}\n\nReturn JSON array of classifications.`
    }],
  });
  return JSON.parse(result.text);
}

3. Model Routing: Smart vs Cheap Model Selection

Not every query needs your most expensive model. Route dynamically based on complexity:

class ModelRouter {
  private models = {
    fast:   { name: 'claude-haiku', costPer1K: 0.00025, quality: 'good' },
    smart:  { name: 'claude-sonnet', costPer1K: 0.003, quality: 'great' },
    best:   { name: 'claude-opus', costPer1K: 0.015, quality: 'best' },
  };

  async route(query: string, context: RoutingContext): Promise<string> {
    // Rule-based routing
    if (context.taskType === 'classification') return this.models.fast.name;
    if (context.taskType === 'code-generation') return this.models.smart.name;
    if (context.taskType === 'complex-reasoning') return this.models.best.name;

    // Complexity-based routing
    const complexity = this.estimateComplexity(query);
    if (complexity < 0.3) return this.models.fast.name;
    if (complexity < 0.7) return this.models.smart.name;
    return this.models.best.name;
  }

  private estimateComplexity(query: string): number {
    // Simple heuristics (could be a small classifier model)
    let score = 0;
    if (query.length > 500) score += 0.2;
    if (query.includes('explain') || query.includes('analyze')) score += 0.2;
    if (query.includes('compare') || query.includes('trade-off')) score += 0.3;
    if ((query.match(/\?/g) || []).length > 2) score += 0.2; // multi-part question
    return Math.min(score, 1);
  }
}

4. Budget Alerts and Hard Caps

class BudgetEnforcer {
  constructor(
    private tracker: CostTracker,
    private limits: {
      dailyCap: number;
      perUserCap: number;
      perRequestCap: number;
    }
  ) {}

  async checkBefore(request: LLMRequest): Promise<BudgetDecision> {
    // Per-request estimate
    const estimatedCost = this.estimateCost(request);
    if (estimatedCost > this.limits.perRequestCap) {
      return { allow: false, reason: `Estimated cost ${estimatedCost} exceeds per-request cap` };
    }

    // Per-user daily limit
    if (request.userId) {
      const userCost = this.tracker.userCostToday(request.userId);
      if (userCost + estimatedCost > this.limits.perUserCap) {
        return { allow: false, reason: 'User daily budget exhausted', degradeTo: 'fast-model' };
      }
    }

    // Global daily limit
    const dailyCost = this.tracker.getTodayCost();
    if (dailyCost + estimatedCost > this.limits.dailyCap) {
      return { allow: false, reason: 'Daily budget exhausted', degradeTo: 'cache-only' };
    }

    return { allow: true };
  }

  private estimateCost(request: LLMRequest): number {
    const inputTokens = countTokens(JSON.stringify(request.messages));
    const estimatedOutputTokens = request.maxTokens || 1000;
    return (inputTokens * 0.003 + estimatedOutputTokens * 0.015) / 1000;
  }
}

Try This Today

Add cost tracking to every LLM call in one project. After 24 hours, answer: (1) what's your total daily spend, (2) which feature costs the most, (3) which user costs the most, (4) what percentage of calls could use a cheaper model. Implement model routing for the cheapest optimization: route simple tasks to Haiku and measure the cost reduction.

Resources

  • OpenRouter — Multi-provider API with built-in cost tracking and model routing
  • Anthropic Usage API — Monitor your Claude API usage and costs programmatically