soif 💧
soif — French for thirst.
Estimate the water footprint of LLM prompts, the way you estimate their cost.
Every LLM answer evaporates real freshwater: data-center cooling towers (on-site) and the
power plants feeding them (off-site) both consume it. Published per-prompt figures span two
orders of magnitude — Google measured 0.26 mL per median Gemini prompt, while Mistral's
lifecycle analysis reports 45 mL per 400-token Large 2 response. soif turns model +
tokens + hosting assumptions into an honest low / mid / high water estimate with a fully
documented, versioned methodology (METHODOLOGY.md).
- Pure Python, zero runtime dependencies, MIT-licensed.
- Library API, CLI, SDK response adapters (OpenAI / Anthropic), a Claude Code hook, and water-aware model routing for agent graphs.
📚 Docs: unchained-labs.github.io/soif
Install
pip install soif-llm # imports as `soif`; from a checkout: pip install .
pip install "soif-llm[tokenizers]" # optional: exact token counts via tiktoken
(The PyPI distribution is soif-llm — the bare name was taken — but the module is soif.)
Quick start
import soif
est = soif.estimate("gpt-4o", prompt="Explain retrieval-augmented generation.")
print(est.humanize())
# ~1.15 mL of water (23.0 drops); range 0.113 mL - 12.46 mL
est.total_ml.mid # millilitres, mid scenario
est.onsite_ml # cooling-tower evaporation at the data center
est.offsite_ml # water consumed generating the electricity
est.embodied_ml # amortised manufacturing (chips, servers, buildings)
est.assumptions # every default the estimate leaned on, spelled out
The accurate path is to feed real token usage from an API response — this captures actual output length, reasoning ("thinking") tokens, and cache hits:
response = client.chat.completions.create(...) # OpenAI — or Anthropic messages.create
est = soif.from_response(response)
Reasoning models drink more — thinking tokens are output tokens:
soif.estimate("gpt-5", output_tokens=500, reasoning_effort="high")
soif.estimate("o3", output_tokens=500, reasoning_tokens=8000) # from real usage
Control the hosting scenario:
soif.estimate("llama-3.1-70b", output_tokens=500,
provider="aws", region="nordics", # presets
include_embodied=False) # operational water only
soif.estimate("my-fine-tune", active_params_b=8, wue=0.2, pue=1.12, ewif=0.4)
CLI
soif estimate "why is the sky blue?" --model claude-sonnet-4-5
soif estimate -m gpt-4o -i 1200 -o 500 --json
soif compare gpt-4o gpt-4o-mini gemini-2.5-flash claude-haiku-4-5 -o 500
soif models
Agent graphs: metering and minimising water across a chain
Two primitives make water a first-class optimization target in agentic pipelines (LangGraph, hand-rolled DAGs, anything):
1. Meter — accumulate across nodes, with a soft budget:
meter = soif.Meter(budget_ml=50)
def summarize_node(state):
resp = client.chat.completions.create(model=state["model"], ...)
meter.record(soif.from_response(resp))
if meter.over_budget:
state["model"] = "gpt-4o-mini" # degrade later hops
return state
print(meter.summary())
# 7 call(s): ~18.2 mL of water (3.7 teaspoons); range ... — within budget (18.2/50.0 mL)
2. soif.optimize — route each node to the least-thirsty capable model:
from soif import optimize
optimize.pick_model(
["gpt-4o", "gpt-4o-mini", "gemini-2.5-flash", "claude-sonnet-4-5"],
min_tier="small", # capability floor for this node
input_tokens=2000, output_tokens=300,
)
# -> 'gemini-2.5-flash'... whichever mid-scenario estimate is lowest
optimize.savings("claude-opus-4", "claude-haiku-4-5", output_tokens=500)
# {'baseline_ml': ..., 'alternative_ml': ..., 'saved_ml': ..., 'saved_pct': ...}
Model choice is the big lever (~30× between tiers); after that: shorter outputs, prompt caching, modest reasoning effort, and low-water regions/providers.
Claude Code hook
Get a water read-out for every session, computed from the transcript's real token usage
— see integrations/claude-code. In short, add to
.claude/settings.json:
{
"hooks": {
"Stop": [
{ "hooks": [{ "type": "command", "command": "soif claude-hook" }] }
]
}
}
After each turn: soif: this session used ~4.31 mL of water (0.9 teaspoons); range ... across 12 model call(s).
How it works (short version)
E_it = tokens × Wh-per-token(model tier) # server energy
E_facility = E_it × PUE # + cooling/power overhead
W_onsite = E_it × WUE # cooling evaporation
W_offsite = E_facility × EWIF # power-plant water
W_total = (W_onsite + W_offsite) × lifecycle # + embodied (optional)
Every factor is a (low, mid, high) triple propagated end-to-end, so the range reflects
genuine uncertainty rather than false precision. Factors are versioned
(soif.factors.FACTORS_VERSION) and calibrated against Google's measured Gemini numbers,
Epoch AI's GPT-4o analysis, Mistral's Large 2 LCA, and Ren et al.'s methodology.
Read METHODOLOGY.md before quoting numbers — these are estimates,
not measurements.
Contributing
Factor updates (new disclosures, better WUE/PUE/EWIF data, new models) are the most
valuable contributions — please include sources. pip install -e ".[dev]" && pytest && ruff check .
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file soif_llm-0.1.0.tar.gz.
File metadata
- Download URL: soif_llm-0.1.0.tar.gz
- Upload date:
- Size: 28.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a36dcf34123548bb37e58354046c79b7270e629f906b33c00bfca68f8f5c1b7
|
|
| MD5 |
5794e5371a354d86a3f3fedd66017684
|
|
| BLAKE2b-256 |
a155d32f170b527662e102542274846681d47f9019ca1c6a54e03400c6733831
|
Provenance
The following attestation bundles were made for soif_llm-0.1.0.tar.gz:
Publisher:
release.yml on Unchained-Labs/soif
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
soif_llm-0.1.0.tar.gz -
Subject digest:
8a36dcf34123548bb37e58354046c79b7270e629f906b33c00bfca68f8f5c1b7 - Sigstore transparency entry: 2534106001
- Sigstore integration time:
-
Permalink:
Unchained-Labs/soif@75fa859483f346a26c05673cf38ebd6df9e5903a -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Unchained-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@75fa859483f346a26c05673cf38ebd6df9e5903a -
Trigger Event:
push
-
Statement type:
File details
Details for the file soif_llm-0.1.0-py3-none-any.whl.
File metadata
- Download URL: soif_llm-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b9cf41ec7a093d5c825e4f5cbe0cc1571c3de915811ef29bb5004d7bcf62797
|
|
| MD5 |
67e7e337f6482ac490e8d2cca53432b8
|
|
| BLAKE2b-256 |
b0c3c212cb7a66946bb58b78cdfeaad4c93a35af619bbdce167b6778c2cc571c
|
Provenance
The following attestation bundles were made for soif_llm-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Unchained-Labs/soif
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
soif_llm-0.1.0-py3-none-any.whl -
Subject digest:
3b9cf41ec7a093d5c825e4f5cbe0cc1571c3de915811ef29bb5004d7bcf62797 - Sigstore transparency entry: 2534106157
- Sigstore integration time:
-
Permalink:
Unchained-Labs/soif@75fa859483f346a26c05673cf38ebd6df9e5903a -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/Unchained-Labs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@75fa859483f346a26c05673cf38ebd6df9e5903a -
Trigger Event:
push
-
Statement type: