429 mid-chain
A 429 from OpenAI mid-chain, where the retry runs on partial reasoning state and confidently returns a wrong answer.
▮ Open source · Apache-2.0 · Single Go binary
Reliability testing for AI agents and AI applications
Fault injection for the agent era.
Exercise the LLM, RAG, and tool-call failures that mocks can't simulate. faultkit wraps any command, injects realistic production failures into its traffic, and captures the exit code, so you run your error paths on purpose instead of discovering them in prod.
AI evals test quality. faultkit tests resilience.
Your evals tell you whether the answer was correct. faultkit tells you whether your application survives failure.
Modern AI apps depend on fragile external systems: LLM providers, vector DBs, embedding and search APIs, tool subprocesses. SDKs handle basic retries. Most application code does not survive the failures retries can't fix.
A 429 from OpenAI mid-chain, where the retry runs on partial reasoning state and confidently returns a wrong answer.
A streaming response that cuts off mid-token and gets treated as final.
A tool subprocess killed before it finishes, leaving the agent loop in an invalid state.
A vector DB returning stale or shuffled results, silently corrupting RAG output.
These happen in production. Unit tests don't catch them. Integration tests against mocks test the mock. Chaos tools inject at the Kubernetes / infra boundary. faultkit injects at the SDK/API boundary, where agent failures actually live.
AI applications fail in new ways, and the old testing playbook does not cover them.
AI applications depend on many external systems (LLMs, vector DBs, search and embedding APIs, tool subprocesses). Every one of them can fail.
Agent workflows are probabilistic, not deterministic. The same input can take a different path, so failures surface in places fixed tests never reach.
Traditional testing falls short. Unit tests and mocks check the happy path and test the mock, not how your app behaves when a real dependency degrades.
Reliability is becoming a production problem. As teams ship agents and AI features, "it broke weirdly in prod" is the failure mode that pages you at 3am.
No daemon, no sidecar, no cluster. faultkit wraps the target process, injects the fault, captures the exit code. Single-shot, built for local dev and CI.
The main mechanism. It carries the LLM, RAG, gateway, and any HTTP(S) scenario. faultkit spins up a localhost proxy with a per-run ephemeral CA; the target trusts it via standard env vars (the proxy injects HTTPS_PROXY + CA env).
Some clients ignore HTTPS_PROXY entirely: Node's fetch/undici, or SDKs running in a filtered subprocess. For those, --base-url points the SDK straight at faultkit via OPENAI_BASE_URL / ANTHROPIC_BASE_URL instead of a proxy setting. No proxy env to honor, no CA to trust.
Runs on macOS and Linux. No special privileges. Serves HTTP/1.1.
A secondary mode for syscall-level faults on Linux, for when you need a failure below the HTTP layer. Loads small BPF programs that hook __x64_sys_* kprobes (e.g. recvmsg, recvfrom, openat) and use bpf_override_return to rewrite the syscall return for processes in the target's PID tree.
Requires Linux 5.8+, x86-64, CAP_BPF (or root), and a kernel with CONFIG_BPF_KPROBE_OVERRIDE (most distro kernels have it).
Each scenario knows exactly what to fire and how to make it look real. Pick one with --scenario, or author your own in YAML. The LLM scenarios fire against every provider faultkit knows — narrow to one with --provider.
429 / 503 from OpenAI + Anthropic with realistic Retry-After headers.
LLM response body replaced with syntactically invalid JSON.
Tool call with malformed or schema-violating arguments, breaking dispatch.
A truncated 200 (finish_reason: length / stop_reason: max_tokens) an agent treats as complete.
SSE chat completion drops mid-token without a [DONE].
HTTP 529 overloaded_error under load — no OpenAI equivalent.
SSE stream emits an error event mid-stream, with no message_stop.
A tool_use block truncated by max_tokens — an incomplete tool call.
200 with stop_reason: refusal — the model declined.
HTTP 413 request_too_large for an oversized request.
ECONNRESET on TCP recvmsg / recvfrom.
EACCES on openat.
Write a scenario in YAML with host/path globs and a fault probability, then pass it with --config scenario.yaml.
List them on your host with faultkit scenario list. More are on the roadmap: rag-stale-results, tool-call-flaky, gateway-timeout, and more.
One binary. No config required to start: point it at a built-in scenario and a command, and read the exit code.
# macOS / Linux (Homebrew)
$ brew install faultkit/tap/faultkit
# Arch (AUR)
$ yay -S faultkit-bin
# Go toolchain
$ go install github.com/faultkit/faultkit/cmd/faultkit@latest
Or download a release binary from GitHub Releases.
# Wrap your suite; faultkit injects 429s into
# the OpenAI / Anthropic calls.
$ faultkit run --scenario llm-api-degraded -- pytest tests/
# Your retry/fallback path runs for real.
# Exit code tells CI what happened:
# 0 = handled the fault
# 1 = your code broke under it
# 3 = no call matched
Terminal summary by default. Add --report report.json to write a JSON artifact for CI. faultkit check reports which modes are available on the host; faultkit scenario list lists built-ins.
Stable across versions, so CI scripts branch on them.
| Code | Meaning |
|---|---|
| 0 | Target passed under fault |
| 1 | Target failed under fault |
| 2 | faultkit internal error |
| 3 | No fault fired, target never hit matched traffic |
| 4 | Usage error |
Everything you need to run faultkit locally and wire it into CI.
faultkit ships as a single static Go binary. Install it through a package manager, the Go toolchain, or grab a release binary.
$ brew install faultkit/tap/faultkit # macOS / Linux (Homebrew)
$ yay -S faultkit-bin # Arch (AUR)
$ go install github.com/faultkit/faultkit/cmd/faultkit@latest
The proxy mode runs on macOS and Linux with no special privileges. eBPF mode is Linux-only and has additional requirements (see below). Run faultkit check after install to see which modes are available on your host.
The core command wraps a target process, injects a fault, and exits with a status code that reflects what happened. Everything after -- is the command faultkit runs.
$ faultkit run [flags] -- <command> [args…]
openai, anthropic); the default fans out across all of them.OPENAI_BASE_URL / ANTHROPIC_BASE_URL instead of HTTPS_PROXY — for clients (Node fetch, filtered subprocesses) that ignore proxy env.auto picks by scenario.By default faultkit prints a terminal summary: which faults fired, how the target behaved, and the final exit code.
For HTTP(S) scenarios (LLM providers, RAG / vector DBs, gateways), faultkit spins up a localhost proxy backed by a per-run ephemeral CA. It injects HTTPS_PROXY and the CA into the target's environment so the target trusts it through standard env vars; no system keychain changes, nothing left behind after the run.
Each request is matched by a host glob and path glob. For requests that match and fire, faultkit synthesizes a vendor-accurate response: correct status, body shape, and streaming behavior. Everything else passes straight through to the real upstream. The target sees a real HTTP 429 over a real TLS connection, because that is exactly what it receives. The proxy serves HTTP/1.1.
For syscall-level faults, faultkit loads small BPF programs that hook __x64_sys_* kprobes (for example recvmsg, recvfrom, and openat) and use bpf_override_return to rewrite the syscall return value for processes in the target's PID tree. All decision logic (probability, matching) lives in Go; the BPF program is a dumb policy enforcer reading an LRU map populated by userspace. The target gets a real ECONNRESET or EACCES straight from the kernel.
Built-ins cover common cases, but you can describe your own with host and path globs and a fault that fires with a given probability. Pass it with --config.
name: stale-vector-search
experiments:
- name: shuffled-results
match:
host: '*.pinecone.io'
path: /query*
fault:
http_status: 200
response_body: '{"matches":[]}'
probability: 0.5 # fire on half of matched requests
$ faultkit run --config scenario.yaml -- pytest tests/test_rag.py
The example above is the raw fault + match form for any host. Built-in LLM scenarios instead use a higher-level failure: mode with an optional provider:, which resolves to a per-provider response fixture — so one scenario covers OpenAI, Anthropic, and any provider added later.
Because faultkit is single-shot and communicates through exit codes, it drops straight into CI. Run it as a step and let the runner branch on the code: exit 1 means your code broke under the fault; exit 3 means no matched traffic ever hit the proxy, which usually means the test didn't call the API you targeted.
- name: Fault injection
run: |
faultkit run --scenario llm-api-degraded --report fault.json -- pytest tests/
code=$?
if [ $code -eq 3 ]; then
echo '::warning::no matched traffic, fault never fired'
fi
exit $code
Start with faultkit check, which reports which modes are available on the host, including whether the kernel supports eBPF kprobe override. If a run exits 3, the target never hit matched traffic: confirm your host/path globs against the real requests with --verbose, which logs every matched request and fired fault.
--verbose.faultkit check; you likely lack CAP_BPF or CONFIG_BPF_KPROBE_OVERRIDE.HTTPS_PROXY in a wrapper script.