LLM Cost Optimization: Оптимизация расходов на LLM

llmcostoptimizationefficiencybudget
← Back to Blog

Введение

Стоимость использования LLM может быстро вырасти при масштабировании. Оптимизация затрат — критическая задача для production систем.


Cost Breakdown

Cost Component         | % of Total | Optimization Potential
-----------------------|------------|----------------------
API/Cloud GPU          | 60-70%     | High (30-50%)
Bandwidth              | 10-15%     | Medium (20-40%)
Storage                | 5-10%      | Low (10-20%)
Engineering            | 15-20%     | Medium (15-25%)
Monitoring/Tools       | 5%         | Low (10-15%)

Token Optimization

Input token reduction

def optimize_input(messages: list[dict]) -> list[dict]:
    """Reduce input tokens without losing context"""
    optimized = []
    
    for msg in messages:
        if msg["role"] == "system":
            msg["content"] = truncate_to_tokens(msg["content"], max_tokens=256)
        elif msg["role"] == "user":
            msg["content"] = remove_filler_phrases(msg["content"])
        elif msg["role"] == "assistant":
            if len(optimized) > 4:
                pass  # summarize older messages
        optimized.append(msg)
    
    return optimized

def remove_filler_phrases(text: str) -> str:
    """Remove common filler phrases"""
    fillers = ["Please", "Could you", "I would like you to", "It would be great if"]
    result = text
    for filler in fillers:
        result = result.replace(f"{filler}, ", "")
    return result.strip()

Output token control

import tiktoken

def count_tokens(text: str, model: str = "gpt-4") -> int:
    encoding = tiktoken.encoding_for_model(model)
    return len(encoding.encode(text))

MODEL_COSTS = {
    "gpt-4o": {"input": 0.0025, "output": 0.010, "unit": "1K tokens"},
    "gpt-4o-mini": {"input": 0.00015, "output": 0.0006, "unit": "1K tokens"},
    "claude-sonnet-4": {"input": 0.003, "output": 0.015, "unit": "1K tokens"},
    "claude-haiku-3.5": {"input": 0.00025, "output": 0.00125, "unit": "1K tokens"},
    "gemini-pro": {"input": 0.0005, "output": 0.0015, "unit": "1K tokens"},
}

Caching Strategies

Prompt-level caching

import hashlib
import json
import time
from typing import Optional

class PromptCache:
    """Cache identical prompts to avoid re-computation"""
    
    def __init__(self, ttl: int = 3600):
        self.ttl = ttl
        self._cache = {}
    
    def _make_key(self, prompt: str, params: dict) -> str:
        key_data = json.dumps({
            "prompt": prompt,
            "params": {k: v for k, v in params.items() if k != "api_key"}
        }, sort_keys=True)
        return hashlib.sha256(key_data.encode()).hexdigest()
    
    def get(self, prompt: str, params: dict) -> Optional[str]:
        key = self._make_key(prompt, params)
        if key in self._cache:
            entry = self._cache[key]
            if time.time() - entry["timestamp"] < self.ttl:
                return entry["result"]
        return None
    
    def set(self, prompt: str, params: dict, result: str):
        key = self._make_key(prompt, params)
        self._cache[key] = {"result": result, "timestamp": time.time()}

# Usage
cache = PromptCache()

def generate(prompt: str, temperature: float = 0.7) -> str:
    cached = cache.get(prompt, {"temperature": temperature})
    if cached:
        return cached  # $0.00 cost
    result = call_llm(prompt, temperature)
    cache.set(prompt, {"temperature": temperature}, result)
    return result

Embedding cache

class EmbeddingCache:
    """Cache embeddings to avoid regenerating them"""
    
    def __init__(self, vector_store, ttl: int = 86400 * 7):
        self.vector_store = vector_store
        self.ttl = ttl
    
    def get_embedding(self, text: str) -> Optional[list]:
        results = self.vector_store.similarity_search(text, k=1, score_threshold=0.999)
        if results:
            return results[0].metadata.get("embedding")
        return None
    
    def store_embedding(self, text: str, embedding: list, doc_id: str):
        self.vector_store.upsert(ids=[doc_id], embeddings=[embedding], documents=[text])

# Cost savings: embedding 1M docs
# Without cache: 1M × $0.00002 = $20
# With cache: ~100K unique × $0.00002 = $2 (90% savings)

Model Selection Strategy

Right-sizing models

MODEL_TIER = {
    "fast": {
        "model": "gpt-4o-mini",
        "cost_per_1k": 0.00015,
        "use_for": ["Classification", "Simple Q&A", "Text extraction", "Routing"]
    },
    "balanced": {
        "model": "claude-haiku-3.5",
        "cost_per_1k": 0.00025,
        "use_for": ["Summarization", "Translation", "Entity extraction"]
    },
    "smart": {
        "model": "claude-sonnet-4",
        "cost_per_1k": 0.003,
        "use_for": ["Complex reasoning", "Code generation", "Analysis"]
    },
    "expert": {
        "model": "gpt-4o",
        "cost_per_1k": 0.0025,
        "use_for": ["Multi-modal", "Advanced reasoning", "Debugging"]
    },
}

def route_to_model(task_type: str, complexity: str) -> str:
    """Route tasks to appropriate model tier"""
    routing = {
        ("classification", "low"): "fast",
        ("classification", "high"): "balanced",
        ("reasoning", "low"): "balanced",
        ("reasoning", "high"): "expert",
        ("summarization", "any"): "fast",
        ("code", "any"): "smart",
    }
    return routing.get((task_type, complexity), "balanced")

Router pattern

class ModelRouter:
    """Route requests to cheapest capable model"""
    
    def __init__(self):
        self.classifier = load_classifier()
        self.models = {
            "fast": OpenAI(model="gpt-4o-mini"),
            "balanced": Anthropic(model="claude-haiku-3.5"),
            "smart": Anthropic(model="claude-sonnet-4"),
            "expert": OpenAI(model="gpt-4o"),
        }
    
    def invoke(self, prompt: str, required_capability: str) -> str:
        tier = self.classifier.predict(prompt, required_capability)
        model = self.models[tier]
        return model.invoke(prompt)
    
    def estimate_cost(self, prompt: str, capability: str) -> float:
        tier = self.classifier.predict(prompt, capability)
        tokens = estimate_tokens(prompt)
        return tokens * MODEL_TIER[tier]["cost_per_1k"] / 1000

# Example: routing saves 60% vs always using gpt-4o
# Without router: 1M requests × gpt-4o = $2,500
# With router: 600K fast + 300K balanced + 100K smart = $950

Batch Processing

Batch API usage

import asyncio
from typing import List

class BatchProcessor:
    """Process requests in batches for cost and rate limit efficiency"""
    
    def __init__(self, llm, batch_size: int = 10, delay: float = 1.0):
        self.llm = llm
        self.batch_size = batch_size
        self.delay = delay
    
    async def process_batch(self, prompts: List[str]) -> List[str]:
        """Process prompts in parallel batches"""
        results = []
        
        for i in range(0, len(prompts), self.batch_size):
            batch = prompts[i:i + self.batch_size]
            
            # Parallel processing
            tasks = [self.llm.ainvoke(p) for p in batch]
            responses = await asyncio.gather(*tasks, return_exceptions=True)
            
            for response in responses:
                if isinstance(response, Exception):
                    results.append(f"Error: {response}")
                else:
                    results.append(response.content)
            
            # Rate limit delay
            await asyncio.sleep(self.delay)
        
        return results
    
    def calculate_batch_savings(self, single_cost: float, batch_count: int) -> float:
        """Batch APIs often have 10-20% discount"""
        batch_discount = 0.15
        return single_cost * batch_discount

# OpenAI Batch API pricing
# Standard: $2.50/1M input tokens, $5/1M output tokens
# Batch: $1.25/1M input tokens, $2.50/1M output tokens (50% off)

Self-Correction and Retry Logic

Cost-aware retry

import random
from typing import Optional

class CostAwareRetry:
    """Smart retry with cost considerations"""
    
    def __init__(self, max_retries: int = 3, base_cost: float = 0.01):
        self.max_retries = max_retries
        self.base_cost = base_cost
        self._spent = 0.0
    
    async def call_with_retry(
        self,
        llm_func,
        prompt: str,
        budget: float = 0.05,
        fallback_func=None
    ) -> Optional[str]:
        for attempt in range(self.max_retries):
            try:
                result = await llm_func(prompt)
                self._validate_result(result)
                return result
            except Exception as e:
                cost = self._estimate_attempt_cost(prompt)
                self._spent += cost
                
                if self._spent >= budget:
                    break
                
                # Exponential backoff + jitter
                delay = (2 ** attempt) * 0.5 + random.uniform(0, 0.5)
                await asyncio.sleep(delay)
        
        # Fallback to cheaper model
        if fallback_func:
            return await fallback_func(prompt)
        
        return None
    
    def _estimate_attempt_cost(self, prompt: str) -> float:
        tokens = len(prompt.split()) / 4  # rough estimate
        return tokens * 0.00015 * 2  # input + output estimate

Local Model Deployment

When to go local

Scenario                    | API Cost/Mo | Local Cost/Mo | Savings
----------------------------|-------------|---------------|--------
100K requests/day           | $2,500      | $300 (GPU)    | 88%
500K requests/day           | $12,500     | $600 (2xGPU)  | 95%
1M+ requests/day            | $25,000     | $1,200 (4xGPU)| 95%
Burst traffic (spiky)       | $3,000      | $500 (base)   | 83%

Local model setup

# Ollama local deployment
# docker-compose.yml
"""
version: '3.8'
services:
  ollama:
    image: ollama/ollama
    ports:
      - "11434:11434"
    volumes:
      - ./models:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
"""

# Cost comparison (monthly, 1M requests)
COST_COMPARISON = {
    "gpt-4o-api": {"cost": 25000, "quality": 10, "latency_ms": 2000},
    "claude-sonnet-api": {"cost": 30000, "quality": 10, "latency_ms": 2500},
    "llama-3-70b-local": {"cost": 300, "quality": 7, "latency_ms": 500},
    "mistral-large-local": {"cost": 300, "quality": 6, "latency_ms": 400},
    "qwen-2.5-72b-local": {"cost": 300, "quality": 8, "latency_ms": 600},
}

Monitoring and Alerts

Cost tracking dashboard

import sqlite3
from datetime import datetime, timedelta

class CostTracker:
    """Track and alert on LLM costs"""
    
    def __init__(self, db_path: str = "costs.db"):
        self.conn = sqlite3.connect(db_path)
        self._init_db()
    
    def _init_db(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS requests (
                id INTEGER PRIMARY KEY,
                timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
                model TEXT,
                input_tokens INTEGER,
                output_tokens INTEGER,
                cost REAL,
                success INTEGER,
                latency_ms INTEGER
            )
        """)
        self.conn.commit()
    
    def log_request(self, model: str, input_tokens: int, 
                    output_tokens: int, cost: float, 
                    success: bool, latency_ms: int):
        self.conn.execute(
            """INSERT INTO requests 
               (model, input_tokens, output_tokens, cost, success, latency_ms)
               VALUES (?, ?, ?, ?, ?, ?)""",
            (model, input_tokens, output_tokens, cost, success, latency_ms)
        )
        self.conn.commit()
    
    def get_daily_cost(self, days: int = 7) -> list:
        cutoff = datetime.now() - timedelta(days=days)
        cursor = self.conn.execute("""
            SELECT DATE(timestamp), SUM(cost), COUNT(*)
            FROM requests
            WHERE timestamp > ?
            GROUP BY DATE(timestamp)
            ORDER BY DATE(timestamp)
        """, (cutoff.isoformat(),))
        return cursor.fetchall()
    
    def get_model_breakdown(self) -> list:
        cursor = self.conn.execute("""
            SELECT model, SUM(cost), COUNT(*), AVG(latency_ms)
            FROM requests
            GROUP BY model
        """)
        return cursor.fetchall()
    
    def check_budget(self, daily_budget: float) -> dict:
        today = datetime.now().strftime("%Y-%m-%d")
        cursor = self.conn.execute("""
            SELECT SUM(cost) FROM requests
            WHERE DATE(timestamp) = ?
        """, (today,))
        spent = cursor.fetchone()[0] or 0
        return {
            "daily_budget": daily_budget,
            "spent_today": spent,
            "remaining": daily_budget - spent,
            "utilization": spent / daily_budget * 100
        }

Optimization Checklist

✅ Token optimization
   - Truncate system prompts
   - Remove filler phrases
   - Use concise language

✅ Caching
   - Prompt-level cache (identical requests)
   - Embedding cache (near-duplicate documents)
   - Response cache (RAG results)

✅ Model selection
   - Route simple tasks to cheap models
   - Use classifier for auto-routing
   - Reserve expensive models for hard tasks

✅ Batch processing
   - Use batch APIs for non-urgent tasks
   - Parallelize independent requests
   - Schedule off-peak processing

✅ Local deployment
   - Move high-volume patterns local
   - Use smaller models for simple tasks
   - Hybrid: local + API fallback

✅ Monitoring
   - Track cost per model per day
   - Set budget alerts at 80%
   - Monitor cost per task type

✅ Architecture
   - Implement graceful degradation
   - Use streaming for long responses
   - Compress responses when possible

Итоги

Оптимизация затрат на LLM — это комбинация: token optimization, caching, right-sizing моделей, batch processing и мониторинга. Начните с tracking — вы не оптимизируете то, что не измеряете.