Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

python-gil-bottleneck-analyzer

Find out why Python code does not scale across cores and pick the right fix: what the GIL does and does not block (I/O, numpy, hashlib, zlib, re, json, Rust tokenizers, switch interval), classify workloads as pure-Python CPU, GIL-releasing native, I/O-bound, memory-bandwidth-bound or lock-contended, measure wall vs CPU time and speedup curves, profile with cProfile, py-spy (--gil, --idle, --native), Scalene, perf -X perf, viztracer, line_profiler, tracemalloc and sys.monitoring, free-threaded 3.13t/3.14t (PEP 703/779, PYTHON_GIL, sys._is_gil_enabled), subinterpreters and InterpreterPoolExecutor, processes, numpy/polars vectorisation, numba nogil, Cython, mypyc, pyo3, OMP_NUM_THREADS oversubscription. Use when threads give no speedup, CPU sits at 100% of one core, a FastAPI endpoint stalls under CPU work, tokenization, JSON parsing, regex cleaning, pandas apply, reranking or data loading is slow, or someone asks whether the GIL is the bottleneck.

Programming / Pythonv1.0.0Updated OCT 21 20267 references2 scripts

Turn "my threads don't make it faster" into a measured diagnosis and the cheapest fix that works. Measure first (wall vs CPU time, a speedup curve across threads and processes), profile to see where time goes and whether that code holds the GIL, classify the workload, then choose: fix the algorithm, vectorise, release the GIL in native code, use processes, use asyncio for waits, or move to the free-threaded build. scripts/gil_probe.py runs a function serially, in threads, in processes and in sub-interpreters and labels the result; scripts/profile_classify.py reads a cProfile or py-spy profile and splits time into GIL-holding, GIL-releasing, waiting and import buckets with ranked recommendations.

Trigger Conditions

Activate this skill when the user:

  • Added threads (ThreadPoolExecutor, threading, asyncio.to_thread, FastAPI def routes) and got no speedup, or a slowdown; or sees one core at 100% while the others idle
  • Asks "is the GIL my bottleneck?", "threads or processes?", "should I try free-threaded / no-GIL Python?", "what are subinterpreters good for?", or "does numpy / hashlib / tokenizers release the GIL?"
  • Has slow CPU-side AI work: tokenizing a corpus, parsing model JSON, cleaning text with regex, pandas apply in feature pipelines, embedding post-processing, CPU reranking, data loaders, server-side batching
  • Runs a FastAPI / asyncio service whose latency spikes when one request does CPU work
  • Has a process pool that is slower than expected because each worker spawns many BLAS/OpenMP/torch threads
  • Wants to read py-spy, Scalene, perf or cProfile output, or set up a CI performance gate

Hangs and deadlocks belong to python-async-deadlock-debugger; designing the pool itself to python-multiprocessing-pool-designer; asyncio structure, rate limits and to_thread to python-asyncio-pattern-advisor.

Workflow

text
1. BASELINE  -> one representative input; record wall time, CPU time (user/sys), RSS, cores busy
2. PROBE     -> python scripts/gil_probe.py --target mod:fn --arg ...   (serial vs threads vs processes)
3. PROFILE   -> py-spy record --native --format raw / python -m cProfile -o out.prof / scalene
4. CLASSIFY  -> python scripts/profile_classify.py out.prof stacks.txt -> bucket shares + verdict
5. CHOOSE    -> algorithm > vectorise/native (release GIL) > processes > free-threaded > threads/asyncio for waits
6. FIX       -> smallest change on the hot path; cap native thread pools; keep one owner of parallelism
7. VERIFY    -> same input, same outputs, best-of-N timings, speedup curve, CI gate on regressions

What the GIL Is and Is Not

On the default (GIL) build of CPython, one thread at a time executes Python bytecode. A running thread is asked to drop the GIL every sys.getswitchinterval() seconds (default 0.005) so others can run, and C code can release it voluntarily around work that does not touch Python objects. So the GIL serialises bytecode and GIL-holding C code, not your program.

OperationGIL during the workThreads scale?
Socket/HTTP I/O, file reads/writes, select/epoll, time.sleep, subprocess waits, lock waitsreleasedyes (asyncio is often simpler)
hashlib on buffers > 2047 bytes, zlib/bz2/lzma compress/decompressreleasedyes
numpy ufuncs, reductions, copies, BLAS (@, dot) on numeric dtypesreleased (mostly)yes, but BLAS is already multithreaded
numpy on dtype=object, np.array(list_of_lists), .tolist()heldno
torch / onnxruntime / most CUDA opsreleased inside the opyes, mind intra-op threads
HF tokenizers encode_batch (Rust, rayon)released; parallel internallyalready parallel
re matching and substitution, json/orjson parse and dump, pickleheldno
sorted, sum, str methods, dict/list ops, any Python loopheldno
pydantic-core validation, most pure-C helpers that build Python objectsheldno

"Released" depends on the library version and input type; verify with scripts/gil_probe.py or py-spy --gil instead of trusting a table. Details: gil-fundamentals.md.

Classify the Workload

ClassSignalsRight fix
CPU-bound pure PythonCPU/wall ≈ 1.0 on one core; threads 1.0x or worse; processes ≈ Nx; profile is Python framesalgorithm first, then vectorise/compile, processes, or free-threaded build
GIL-releasing nativeCPU/wall ≈ N with N threads; threads ≈ processesthreads (or the library's own pool); cap nested threads
I/O-boundCPU/wall ≪ 1 (e.g. < 0.25); time in select/recv/sleepasyncio with a Semaphore, or threads; processes add nothing
Memory-bandwidth-boundmany cores busy, speedup flattens at 2-4x for threads and processes; large arraysfewer workers, float32, fuse passes, chunk to fit cache
Lock-contendedcores busy or idle, speedup flat, py-spy shows acquire / take_gilshrink critical sections, shard state, per-worker copies

Mixed workloads are common (download, parse, embed, upsert): split the stages and give each its own tool.

Measure Before Changing Anything

python
import os
import time


def measure(fn, *args):
    """Return (wall_s, cpu_s, cpu_per_wall) for one call; cpu covers every thread of this process."""
    wall0, cpu0 = time.perf_counter(), time.process_time()
    fn(*args)
    wall, cpu = time.perf_counter() - wall0, time.process_time() - cpu0
    return wall, cpu, cpu / wall if wall else 0.0


def busy(n):
    return sum(i * i for i in range(n))


wall, cpu, ratio = measure(busy, 2_000_000)
print(f"wall={wall:.3f}s cpu={cpu:.3f}s cores_busy={ratio:.2f}")
print("user/sys:", os.times().user, os.times().system)

Read it: cores_busy ≈ 1 CPU-bound on one core; ≈ N parallel; ≪ 1 waiting. High sys time points at syscalls, page faults or lock thrashing. Outside the process: /usr/bin/time -v python app.py (user, sys, elapsed, max RSS, context switches), top -H -p PID or pidstat -t -p PID 1 for per-thread CPU, and inside a thread time.thread_time(). Then draw the speedup curve: the same N tasks at 1, 2, 4, 8 threads and processes (harness in measuring-and-profiling.md). Thread speedup flat at 1.0x while processes climb is the GIL fingerprint.

Profile: Which Tool Answers Which Question

QuestionTool and command
Which functions take the time (deterministic)?python -m cProfile -o out.prof app.py, then python -m pstats out.prof (sort cumtime, stats 20)
Where is a running process right now, all threads?py-spy dump --pid PID / py-spy top --pid PID
Flame graph with low overhead, prod-safepy-spy record -o flame.svg --pid PID (--threads, --subprocesses)
Is the hot code holding the GIL?py-spy record --gil -o gil.svg -- python app.py (only GIL-holding samples)
Are threads idle/waiting?py-spy record --idle ... keeps idle samples
Python vs C/Rust time splitpy-spy record --native, or scalene app.py (Python / native / system columns)
Kernel + native + Python in one stack (Linux, 3.12+)perf record -F 999 -g -- python -X perf app.py; perf report
Timeline of threads, GIL hand-offs, async tasksviztracer app.py then vizviewer result.json
Which line in a hot function?@profile + kernprof -l -v app.py (line_profiler)
Memory growth and allocation sitestracemalloc.start(25) + snapshot diff; Scalene memory columns
Cheap custom counters (3.12+)sys.monitoring callbacks on PY_START / CALL

Feed the result to scripts/profile_classify.py: py-spy record --format raw -o stacks.txt --native -- python app.py, then python scripts/profile_classify.py stacks.txt. Exact flags, output reading and pitfalls (cProfile only sees its own thread; operator calls on arrays are charged to the caller) are in measuring-and-profiling.md.

Situation -> Right Tool

SituationChooseWhy / caveat
O(n²) loop, repeated work, needless conversionsfix the algorithm10-1000x; parallelism multiplies waste
Numeric loop over arrays / columnsnumpy / polars / pandas column opsruns in C, releases the GIL, often 10-100x
Custom numeric kernel that resists vectorisingnumba @njit(nogil=True)compiled, GIL-free, threads then scale
Typed hot module, C-level loopsCython with nogil: or mypycCython can release the GIL; mypyc speeds up but keeps it
Heavy string/parsing logic, long-lived libraryRust + pyo3 (allow_threads / detach)fastest, release the GIL around pure-Rust work
Pure-Python CPU per item ≥ ~1 ms, items independentprocesses (pool skill)one GIL each; pay pickling and startup
Network waits (LLM APIs, vector DB, scraping)asyncio (asyncio skill) or threadswaiting needs no cores
GIL-releasing native work (hashing, compression, numpy, decode)threadsno pickling, shared memory
Pure-Python CPU, shared in-memory state, all deps support itfree-threaded 3.14t + threadssupported but optional build (PEP 779); verify extensions
Isolation without processes, stdlib-only code (3.14)InterpreterPoolExecutorone GIL per interpreter; many C extensions cannot load there
Big batched tensor mathGPU (torch, CuPy)move the batch, not single items

Decision tables, code for each hatch and when each loses: escape-hatches.md.

Symptom -> Cause -> Fix

SymptomLikely causeFix
8 threads, same wall time as 1, one core at 100%pure-Python CPU holding the GILprocesses, vectorise, or free-threaded build
Threads slower than serialGIL convoy: CPU threads + switch-interval hand-offsfewer CPU threads; processes
Event loop latency spikes when one request is heavyCPU work in async def (or GIL-holding work in the threadpool)run_in_executor(process_pool, ...), worker queue
Process pool: all cores 100% but slower than 4 workersBLAS/OpenMP/torch threads per worker (oversubscription)OMP_NUM_THREADS=1, torch.set_num_threads(1) per worker
Speedup flattens at ~3x whatever you domemory bandwidth, or a serial parent stageprofile parent; fewer, fatter tasks; float32
Profile shows acquire / take_gil / futex at the toplock or GIL contentionsmaller critical sections; processes
Free-threaded build still not parallelan extension re-enabled the GIL on importcheck sys._is_gil_enabled(); RuntimeWarning at import names the module
Free-threaded build is slower single-threadedexpected overhead (larger in 3.13 than 3.14)only worth it when you actually use threads
tokenizers warning "process just got forked ... Disabling parallelism"rayon pool used before forkTOKENIZERS_PARALLELISM=false in workers, or spawn
Huge sys time, many context switchesoversubscription or lock thrashcap threads; perf stat -e context-switches

More cases and the oversubscription arithmetic: troubleshooting-oversubscription-and-contention.md.

Free-Threaded Python and Subinterpreters (Precise Status)

VersionStatus
3.12PEP 684 per-interpreter GIL (C API only); PEP 669 sys.monitoring; -X perf
3.13PEP 703 free-threaded build is experimental: separate python3.13t binary; sys._is_gil_enabled(); PYTHON_GIL / -X gil; noticeably slower single-threaded (specializing interpreter disabled in that build)
3.14PEP 779: free-threaded build is officially supported but still optional, not the default (python3.14t); single-thread penalty much smaller; PEP 734 concurrent.interpreters and concurrent.futures.InterpreterPoolExecutor
python
import sys
import sysconfig

free_threaded_build = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
gil_fn = getattr(sys, "_is_gil_enabled", None)   # 3.13+
gil_on = gil_fn() if gil_fn else True
print(f"free-threaded build: {free_threaded_build}, GIL enabled now: {gil_on}")

Rules: check sys._is_gil_enabled() after importing everything, because importing an extension that has not declared free-threading support re-enables the GIL with a RuntimeWarning (PYTHON_GIL=0 or -X gil=0 forces it off, at your own risk). The build removes the GIL, not races: counter += 1 and check-then-act on shared dicts still need a threading.Lock. Subinterpreters give one GIL per interpreter in one process, but extensions must support them (numpy 2.3 raised ImportError inside one when this was written), and objects cross by pickling or shareable types. Full details: free-threading-and-subinterpreters.md.

Oversubscription: One Owner of Parallelism

Total runnable threads ≈ processes × (Python threads + native pool threads). Keep it ≤ usable cores.

python
import os

# Set BEFORE importing numpy/torch/tokenizers in each worker process (pool initializer or top of module).
for var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", "NUMEXPR_NUM_THREADS"):
    os.environ.setdefault(var, "1")
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
# import torch; torch.set_num_threads(1)

Pick one level: either N processes × 1 native thread, or 1 process × N native threads. Count usable cores with len(os.sched_getaffinity(0)) (Linux) or os.process_cpu_count() (3.13+); neither sees a cgroup CPU quota, so read /sys/fs/cgroup/cpu.max in containers.

AI-Engineering Cases

CaseUsual diagnosisFix
Tokenizing a corpusHF fast tokenizers already parallel (Rust, GIL released); slow Python tokenizers hold the GILencode_batch on large batches; processes only for Python tokenizers; never both
JSON parsing of model outputsjson.loads holds the GIL; parse cost ≈ pickling costorjson/msgspec in place; parallelism rarely pays
Embedding post-processingper-row Python loops (normalise, top-k)one numpy call on the batch (np.linalg.norm(axis=1), argpartition)
Batching in inference serversPython collation/padding per request on the event looppre-tokenize to arrays, batch in one task, move CPU work off the loop
pandas apply(axis=1) in featuresa Python call per rowcolumn ops, np.where, .str/.dt, polars expressions
Regex cleaning for corporare holds the GILmerge/precompile, prefilter, processes per shard; regex module concurrent=True
CPU rerankingcross-encoder in torch (GIL released) vs Python BM25 loops (held)batch the model; vectorise scoring; cap torch threads
Data loadersdecode/augment in PythonDataLoader(num_workers=...) processes; threads for GIL-releasing decoders
FastAPI endpoint doing CPU workasync def blocks the loop; def competes for the GILprocess pool via run_in_executor, or a job queue
python
import asyncio
from concurrent.futures import ProcessPoolExecutor
from typing import Optional

pool: Optional[ProcessPoolExecutor] = None   # created once in the app lifespan


def rerank_cpu(query: str, docs: list[str]) -> list[int]:
    """Module-level so it pickles: pure-Python scoring that holds the GIL."""
    q = set(query.lower().split())
    scores = [len(q & set(d.lower().split())) for d in docs]
    return sorted(range(len(docs)), key=scores.__getitem__, reverse=True)


async def rerank_endpoint(query: str, docs: list[str]) -> list[int]:
    loop = asyncio.get_running_loop()
    return await loop.run_in_executor(pool, rerank_cpu, query, docs)   # event loop stays responsive

Worked versions of every case: ai-engineering-performance.md.

Verify the Fix

  1. Same input, same output: assert results match the serial version (order-insensitive where appropriate).
  2. Time best-of-N (timeit.repeat, --repeat 5) on an idle machine; report median and spread, not one run.
  3. Re-run the speedup curve: the fix should move the thread or process line, not just the 1-worker point.
  4. Check cores busy (cpu/wall), RSS and sys time did not explode.
  5. Gate regressions in CI with ratios (speedup ≥ X, Python CPU share ≤ Y%), not absolute seconds.

Statistics, pytest-benchmark, perf gates: testing-and-benchmarking.md.

Output Format

When diagnosing, answer with:

  1. Measurements - wall, CPU, cores busy, the speedup table (threads vs processes) and the interpreter (GIL build or free-threaded, sys._is_gil_enabled()); paste gil_probe.py output when available.
  2. Classification - one class from the table above, with the evidence (profile shares from profile_classify.py, hot functions, GIL-holding vs releasing).
  3. Fix - ranked options with expected impact and confidence; complete before/after code for the chosen one (including thread caps and pool lifecycle).
  4. Verification - how to confirm: identical outputs, best-of-N timing, new speedup curve, CI gate.
  5. Caveats - version-specific behaviour (3.13 vs 3.14), extension compatibility, noise in the numbers.

Scripts

bash
# Classify a profile (static: never runs the profiled code)
python -m cProfile -o out.prof ingest.py
python scripts/profile_classify.py out.prof                          # pstats file
py-spy record --format raw --native -o stacks.txt -- python ingest.py
python scripts/profile_classify.py stacks.txt --top 15               # py-spy / perf collapsed stacks
python scripts/profile_classify.py out.prof --collapsed stacks.txt   # both; shares are averaged
cat stacks.txt | python scripts/profile_classify.py -                # stdin
python scripts/profile_classify.py stacks.txt --format collapsed --json
python scripts/profile_classify.py stacks.txt --fail-if-python-cpu-over 50   # CI gate, exit 1
python scripts/profile_classify.py --demo                            # profiles a built-in workload

Buckets: python-cpu, native-hold, native-unknown, native-release, io-wait, gil-wait, import. Prints share of time, top hot spots (with call counts for pstats), per-thread shares (py-spy --threads), a verdict (GIL-BOUND, GIL-CONTENDED, I/O-BOUND, GIL-RELEASING NATIVE, STARTUP-DOMINATED, MIXED) and ranked recommendations with confidence. A pstats file written by another Python version that cannot be loaded exits 2 with a "regenerate with this interpreter" message. Flags: --top N, --json, --format {auto,pstats,collapsed}, --collapsed FILE, --fail-if-python-cpu-over PCT, --demo. Exit codes: 0 analysed, 1 gate failed, 2 usage/read/load error.

bash
# Probe scaling (EXECUTES the target in a child process group, hard-killed after --timeout)
python scripts/gil_probe.py --builtin pure-python          # pure-python|native-release|native-hold|io|mixed
python scripts/gil_probe.py --target ingest.clean:normalize --arg '"some text"' --arg 3
python scripts/gil_probe.py --target ./bench.py:score_batch --arg 2000 --workers 8 --repeat 5 --interpreters
python scripts/gil_probe.py --builtin io --scale 0.5 --timeout 60 --json
python scripts/gil_probe.py --target ingest.clean:normalize --fail-if-gil-bound   # exit 1 if GIL-bound

Runs N tasks serially, in N threads, in N processes (forkserver/spawn, pre-warmed) and with --interpreters in an InterpreterPoolExecutor (3.14+); reports wall, median, CPU, CPU/wall, per-worker CPU, speedup, CPU availability (how busy the machine is), the interpreter (GIL build or free-threaded, GIL enabled) and a classification: "GIL-bound: scales with processes but not threads", "releases the GIL", "I/O-bound", "mixed I/O + CPU", "CPU-bound, does not scale", or a low-confidence "likely GIL-bound" when the machine is too busy. Flags: --target MODULE:FUNC or --builtin NAME, --arg VALUE (Python literal, repeatable), --workers, --repeat, --scale, --interpreters, --timeout (default 300 s), --fail-if-gil-bound, --json. Exit codes: 0 measured, 1 gate failed, 2 usage error, target error or timeout.

Additional Resources

FileContent
gil-fundamentals.mdHow the GIL works, switch interval, what releases it (stdlib, numpy, torch, tokenizers), atomicity myths, version history
measuring-and-profiling.mdWall vs CPU, user/sys, per-thread CPU, speedup curves, cProfile, py-spy, Scalene, perf, viztracer, line_profiler, tracemalloc, sys.monitoring
free-threading-and-subinterpreters.md3.13t vs 3.14t status, PYTHON_GIL, extension compatibility, thread safety, concurrent.interpreters, InterpreterPoolExecutor
escape-hatches.mdDecision table and code for algorithms, numpy/polars, numba, Cython, mypyc, Rust/pyo3, processes, GPU, asyncio
ai-engineering-performance.mdTokenization, JSON outputs, embeddings, server batching, pandas, regex corpora, reranking, data loaders, FastAPI
troubleshooting-oversubscription-and-contention.mdSymptom tables, thread-pool env vars, torch threads, cgroups, lock and GIL contention, memory bandwidth
testing-and-benchmarking.mdtimeit, pytest-benchmark, noise and statistics, speedup assertions, CI perf gates, testing free-threaded code
More in Python

python-async-deadlock-debugger

Diagnose and fix hangs, deadlocks, livelocks and starvation in asyncio, threaded and multiprocessing Python: classify CPU spin vs blocked IO vs deadlock vs lost wakeup vs event loop blocked by sync code, collect evidence with faulthandler, py-spy dump, gdb py-bt, asyncio.all_tasks, python -m asyncio ps/pstree, debug mode and sys._current_frames, then fix lock-order cycles, non-reentrant asyncio.Lock re-acquisition, run_coroutine_threadsafe().result() on the loop thread, Queue.join without task_done, Condition lost wakeups, semaphore leaks, futures nobody resolves, nested event loops, thread pool exhaustion, multiprocessing Queue/fork deadlocks and shutdown hangs. Ships a static hang scanner and a watchdog that dumps every thread and asyncio task.

python-asyncio-pattern-advisor

Recommend, implement and review Python asyncio patterns: async vs threads vs processes (IO vs CPU-bound), coroutines vs tasks vs futures, TaskGroup, gather and return_exceptions, wait and as_completed, Semaphore and Queue worker pools with backpressure, asyncio.timeout vs wait_for, cancellation, shield and cleanup, token-bucket RPM/TPM rate limits for LLM APIs, retries with backoff and jitter, async generators and SSE token streaming, to_thread/run_in_executor, background task references, eager tasks, asyncio.run/Runner, uvloop, Jupyter, FastAPI async vs def routes, httpx.AsyncClient lifespan, async DB/vector-store clients, debug mode and pytest-asyncio.

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.