Files
autoreg-skills-max/skills/account-validation/references/llm-api-benchmarking.md

5.2 KiB

LLM API Benchmarking & Stress Testing

Patterns for benchmarking OpenAI-compatible LLM APIs with concurrent requests, latency measurement, and throughput analysis. Developed from aiand.com stress testing.

When to use

  • User asks to benchmark/models/throughput of an LLM API
  • User wants concurrent stress test of API keys
  • User asks to compare model performance (latency, TPS, tokens)
  • User has a pool of API keys and wants to test capacity

Curl-based approach (bypasses Cloudflare)

Many LLM API providers use Cloudflare protection that blocks Python HTTP libraries (requests, aiohttp, urllib). The system curl binary passes through because it uses native TLS and different fingerprinting. Use subprocess.run:

import subprocess, json, time

def call_curl(key, model, prompt, api_url, max_tokens=50):
    body = json.dumps({
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens
    })
    t0 = time.perf_counter()
    r = subprocess.run([
        "curl", "-s", "-o", "-", "-w", "|%{http_code}|%{time_total}",
        "-X", "POST", api_url,
        "-H", f"Authorization: Bearer {key}",
        "-H", "Content-Type: application/json",
        "-d", body,
        "--max-time", "45"
    ], capture_output=True, text=True, timeout=50)
    t = time.perf_counter() - t0
    
    # Parse: body text before last two |-separated fields
    out = r.stdout.strip()
    parts = out.rsplit('|', 2)
    if len(parts) >= 3:
        body_text, http_code, curl_time = parts[0], parts[1], parts[2]
        if http_code == "200":
            data = json.loads(body_text)
            msg = data["choices"][0]["message"]
            # Check BOTH content and reasoning (reasoning models)
            txt = msg.get("content") or msg.get("reasoning", "")
            tok = data.get("usage", {}).get("completion_tokens", 0)
            return {"ok": True, "latency": round(t, 3), "tokens": tok, "output": txt[:50]}
    return {"ok": False, "error": "parse"}

Concurrent stress test pattern

Use ThreadPoolExecutor with subprocess (not asyncio with aiohttp — blocked by Cloudflare):

import concurrent.futures, statistics, time

def stress_test(keys, model, api_url, concurrency=3):
    """Run concurrency requests per key, measure throughput."""
    all_results = []
    t0 = time.perf_counter()
    
    with concurrent.futures.ThreadPoolExecutor(max_workers=min(len(keys)*concurrency, 50)) as ex:
        futures = []
        for key in keys:
            for _ in range(concurrency):
                futures.append(ex.submit(call_curl, key, model, "Say hi", api_url))
        for f in concurrent.futures.as_completed(futures):
            all_results.append(f.result())
    
    total_t = time.perf_counter() - t0
    oks = [r for r in all_results if r.get("ok")]
    
    return {
        "total": len(all_results),
        "ok": len(oks),
        "fail": len(all_results) - len(oks),
        "time": total_t,
        "throughput": len(oks)/total_t,
        "latency_avg": statistics.mean([r["latency"] for r in oks]) if oks else 0,
        "latency_p50": statistics.median([r["latency"] for r in oks]) if oks else 0,
        "latency_min": min([r["latency"] for r in oks]) if oks else 0,
        "latency_max": max([r["latency"] for r in oks]) if oks else 0,
        "total_tokens": sum([r["tokens"] for r in oks]) if oks else 0,
        "total_tps": sum([r["tokens"] for r in oks])/total_t if oks else 0,
    }

Metrics to collect

Metric What it tells you
throughput (req/s) API capacity under load
latency_avg Typical response time
latency_p50 Median — better than avg for skewed distributions
latency_min/max Range — high max = tail latency issues
total_tps (tok/s) Real token generation speed
fail rate Reliability under concurrency

Key parsing from bulk files

Common format: email|sk-xxx|org_id. Don't split once and treat sk-xxx|org_id as the token:

[REDACTED] def load_keys(path): keys = [] for line in Path(path).read_text().strip().split('\n'): parts = line.strip().split('|') for p in parts: if p.startswith('sk-') and len(p) > 40: keys.append(p) break return keys


## GitHub tools for benchmarking

Quick-start tools that can be cloned and adapted:
- `theodoreniu/llmperf` — Python/Streamlit, comprehensive LLM benchmarking
- `gouwsxander/stress-test` — Python/aiohttp, simple stress testing (needs curl adaptation for CF-protected APIs)
- `Yoosu-L/llmapibenchmark` — Go-based, measures API throughput across concurrency levels

## Pitfalls

1. **Python HTTP blocked by Cloudflare** — always use `subprocess` + `curl` for CF-protected APIs
2. **Reasoning models return null content** — check `reasoning` field, not just `content`
3. **Model list ≠ working models** — `GET /v1/models` may return models that don't exist on the host
4. **Terminal truncates long keys** — verify file contents, not terminal output
5. **ThreadPoolExecutor max_workers** — cap at 50-100 to avoid overwhelming the local system
6. **curl timeout** — set `--max-time` higher than expected latency to avoid false failures