soif-llm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: pip install -e ".[dev]"
21
+ - name: Lint
22
+ run: ruff check .
23
+ - name: Test
24
+ run: pytest -q
@@ -0,0 +1,51 @@
1
+ name: Docs
2
+
3
+ # Builds the MkDocs site and deploys it to GitHub Pages:
4
+ # https://unchained-labs.github.io/soif/
5
+ # Pages must be set to "GitHub Actions" as the source (configure-pages
6
+ # attempts to enable it automatically on first run).
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: read
15
+ pages: write
16
+ id-token: write
17
+
18
+ concurrency:
19
+ group: pages
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ build:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: "3.12"
30
+ - name: Build site
31
+ run: |
32
+ pip install mkdocs-material
33
+ cp METHODOLOGY.md docs/methodology.md
34
+ cp integrations/claude-code/README.md docs/claude-code.md
35
+ mkdocs build
36
+ - uses: actions/configure-pages@v5
37
+ with:
38
+ enablement: true
39
+ - uses: actions/upload-pages-artifact@v3
40
+ with:
41
+ path: site/
42
+
43
+ deploy:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment:
47
+ name: github-pages
48
+ url: ${{ steps.deployment.outputs.page_url }}
49
+ steps:
50
+ - id: deployment
51
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,48 @@
1
+ name: Release to PyPI
2
+
3
+ # Publishes soif-llm to PyPI via Trusted Publishing (OIDC, no API tokens).
4
+ # One-time setup on pypi.org: add a "pending publisher" for project
5
+ # `soif-llm`, owner `Unchained-Labs`, repo `soif`, workflow `release.yml`,
6
+ # environment `pypi`. Then: create a GitHub release (or push a tag `v*`).
7
+
8
+ on:
9
+ release:
10
+ types: [published]
11
+ push:
12
+ tags: ["v*"]
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - name: Build sdist and wheel
23
+ run: |
24
+ pip install build
25
+ python -m build
26
+ - name: Check metadata
27
+ run: |
28
+ pip install twine
29
+ twine check dist/*
30
+ - uses: actions/upload-artifact@v4
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/soif-llm
41
+ permissions:
42
+ id-token: write
43
+ steps:
44
+ - uses: actions/download-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ site/
8
+ docs/methodology.md
9
+ docs/claude-code.md
soif_llm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Unchained Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ # Methodology
2
+
3
+ `soif` estimates the freshwater **consumed** (evaporated or otherwise removed from the
4
+ local watershed — not merely withdrawn and returned) to serve an LLM response. This
5
+ document explains the model, the factors, their sources, and the limits of the whole
6
+ exercise. Factor values live in [`src/soif/factors.py`](https://github.com/Unchained-Labs/soif/blob/main/src/soif/factors.py) and are
7
+ versioned via `FACTORS_VERSION`.
8
+
9
+ ## 1. The model
10
+
11
+ Following the operational-water methodology of Ren et al. ([*Making AI Less "Thirsty"*,
12
+ arXiv:2304.03271](https://arxiv.org/abs/2304.03271)), with an optional embodied adder:
13
+
14
+ ```
15
+ E_it = (output_tokens + reasoning_tokens
16
+ + 0.1 × input_tokens + 0.01 × cached_tokens) / 1000 × Wh_per_1k(tier)
17
+ E_facility = E_it × PUE
18
+ W_onsite = E_it × WUE # cooling-tower / evaporative cooling at the DC
19
+ W_offsite = E_facility × EWIF # water consumed generating the electricity
20
+ W_embodied = (W_onsite + W_offsite) × (lifecycle_multiplier − 1)
21
+ W_total = W_onsite + W_offsite + W_embodied
22
+ ```
23
+
24
+ Units work out neatly: **1 L/kWh ≡ 1 mL/Wh**, so Wh × (L/kWh) yields millilitres.
25
+
26
+ Every factor is a **(low, mid, high)** scenario triple and the triples are multiplied
27
+ through, so the reported range is a best-case/central/worst-case *scenario spread*, not a
28
+ statistical confidence interval. This is deliberate: public per-prompt figures disagree by
29
+ ~100× and pretending otherwise would be false precision.
30
+
31
+ ## 2. Energy per token
32
+
33
+ Direct measurement is impossible from outside a provider, so `soif` buckets models into
34
+ five **tiers by active parameter count** (MoE models count activated experts only), each
35
+ with a server-side (IT) energy range per 1000 **output** tokens:
36
+
37
+ | Tier | Active params | Wh / 1k output tokens (low/mid/high) | Examples |
38
+ |---|---|---|---|
39
+ | nano | < 3B | 0.02 / 0.06 / 0.15 | gemini-flash-lite, gpt-5-nano |
40
+ | small | 3–15B | 0.06 / 0.20 / 0.50 | gpt-4o-mini, gemini-flash, llama-8b |
41
+ | medium | 15–70B | 0.15 / 0.50 / 1.20 | claude-haiku, deepseek-v3 (~37B active) |
42
+ | large | 70–250B | 0.30 / 0.80 / 2.00 | gpt-4o, claude-sonnet, mistral-large |
43
+ | frontier | > 250B / premium | 0.80 / 2.00 / 5.00 | gpt-5, o3, claude-opus, grok |
44
+
45
+ Calibration anchors:
46
+
47
+ - **Epoch AI** estimates ~**0.3 Wh** for a typical GPT-4o query with ~500 output tokens
48
+ (server power, ex-PUE) → 0.6 Wh/1k, inside our *large* band (0.3–2.0, mid 0.8).
49
+ ([epoch.ai](https://epoch.ai/gradient-updates/how-much-energy-does-chatgpt-use))
50
+ - **Google** measured **0.24 Wh** per *median* Gemini Apps prompt in May 2025 — full stack,
51
+ including idle machines and PUE ([arXiv:2508.15734](https://arxiv.org/abs/2508.15734)).
52
+ A small-tier 500-token response at mid scenario gives 0.10 Wh IT / 0.11 Wh facility;
53
+ the median production prompt plausibly sits between our small mid and high scenarios.
54
+ - Prefill (input) tokens are batched and compute-bound rather than memory-bound; we charge
55
+ them **10%** of an output token, and **1%** for cache-read tokens. Reasoning/thinking
56
+ tokens are decode work and are charged at the full output rate.
57
+
58
+ ## 3. On-site water (WUE) and overhead (PUE)
59
+
60
+ WUE = litres evaporated on-site per kWh of IT energy; PUE = facility/IT energy. Provider
61
+ presets from public disclosures:
62
+
63
+ | Provider preset | WUE L/kWh (low/mid/high) | PUE (low/mid/high) | Basis |
64
+ |---|---|---|---|
65
+ | aws | 0.10 / 0.18 / 0.40 | 1.10 / 1.15 / 1.25 | AWS reported fleet WUE 0.15–0.18 |
66
+ | azure | 0.30 / 0.49 / 0.80 | 1.12 / 1.18 / 1.30 | Microsoft reported ~0.49 |
67
+ | gcp | 0.90 / 1.10 / 1.40 | 1.09 / 1.10 / 1.15 | Google fleet ~1.1 L/kWh, PUE 1.10 |
68
+ | average | 0.30 / 1.00 / 1.90 | 1.20 / 1.40 / 1.60 | industry surveys (Uptime ~1.56 PUE) |
69
+
70
+ OpenAI models default to `azure`, Anthropic to `aws`, Google to `gcp`, open-weight and
71
+ unknown models to `average`. Override with `provider=` or raw `wue=` / `pue=`.
72
+
73
+ ## 4. Off-site water (EWIF)
74
+
75
+ Electricity generation consumes water (thermoelectric cooling, hydro reservoir
76
+ evaporation). Regional consumption-weighted factors (L/kWh) follow Ren et al. / Macknick
77
+ et al.; ranges are wide because grid mix and season dominate:
78
+
79
+ | Region | EWIF L/kWh (low/mid/high) |
80
+ |---|---|
81
+ | world (default) | 0.40 / 1.50 / 3.20 |
82
+ | us | 0.50 / 1.60 / 3.10 |
83
+ | eu | 0.30 / 1.20 / 2.50 |
84
+ | france | 0.50 / 1.50 / 3.00 |
85
+ | nordics | 0.05 / 0.30 / 0.90 |
86
+ | asia | 0.50 / 1.90 / 3.50 |
87
+ | renewable (matched wind/solar PPA) | 0.001 / 0.10 / 0.50 |
88
+
89
+ Note that "market-based" corporate accounting (buying renewable certificates) does not
90
+ remove the physical water use of the local grid; `soif` models physical (location-based)
91
+ water. Use `region="renewable"` only for genuinely co-located/matched supply.
92
+
93
+ ## 5. Embodied water
94
+
95
+ Chip fabrication (ultra-pure water in fabs), server manufacturing, and data-center
96
+ construction consume water that should be amortised over the hardware's useful life.
97
+ Public data is thin; Mistral's Large 2 LCA — **45 mL per 400-token response**, lifecycle —
98
+ implies embodied + upstream shares far exceeding operational water, while Google's
99
+ operational figure is ~170× lower. `soif` applies a deliberately wide lifecycle
100
+ multiplier of **1.1× / 1.5× / 3.0×** on operational water, reported separately and
101
+ removable via `include_embodied=False`.
102
+
103
+ ## 6. Cross-checks against the literature
104
+
105
+ For a ~500-token response, mid scenarios (embodied included):
106
+
107
+ | Case | soif mid (range) | Published |
108
+ |---|---|---|
109
+ | gemini-2.5-flash, gcp | ≈0.4 mL (0.08–2.5) | Google: 0.26 mL (operational, median prompt) |
110
+ | gpt-4o, azure/us | ≈1.4 mL (0.14–15) | Epoch: 0.3 Wh ⇒ ~0.5–1.5 mL depending on WUE/EWIF |
111
+ | mistral-large, france | ≈1.5 mL (0.2–16) | Mistral LCA: 45 mL (full lifecycle, incl. training amortisation) |
112
+ | GPT-3-era frontier, worst case | high tail 15–50 mL | Ren et al.: 10–50 mL per medium response |
113
+
114
+ The spread between Google and Mistral is largely **scope** (operational vs. full
115
+ lifecycle including amortised training, which `soif` does not include) and **methodology**
116
+ (measured medians vs. LCA attribution). `soif`'s ranges are designed to bracket the
117
+ defensible literature, with the mid scenario tracking measured operational figures plus a
118
+ moderate embodied adder.
119
+
120
+ ## 7. What this is not
121
+
122
+ - **Not a measurement.** Only providers can measure; everything here is estimation from
123
+ public data.
124
+ - **Training and R&D amortisation are excluded** (Mistral includes them; Google excludes
125
+ them). They can dominate lifecycle numbers for low-traffic models.
126
+ - **Water stress is not weighted.** A millilitre in a water-stressed basin matters more
127
+ than one in a rainy region; see "Not All Water Consumption Is Equal"
128
+ ([arXiv:2506.22773](https://arxiv.org/abs/2506.22773)) for a stress-weighted approach —
129
+ a good future extension.
130
+ - **Factors age fast.** Hardware efficiency improved ~33× year-over-year in Google's
131
+ disclosure window. Check `FACTORS_VERSION`, and file an issue/PR when better data lands.
132
+
133
+ ## Sources
134
+
135
+ - Li, Yang, Islam, Ren — *Making AI Less "Thirsty"* — [arXiv:2304.03271](https://arxiv.org/abs/2304.03271) / [CACM](https://cacm.acm.org/sustainability-and-computing/making-ai-less-thirsty/)
136
+ - Google — *Measuring the environmental impact of delivering AI at Google scale* — [arXiv:2508.15734](https://arxiv.org/abs/2508.15734) / [blog](https://cloud.google.com/blog/products/infrastructure/measuring-the-environmental-impact-of-ai-inference)
137
+ - Epoch AI — *How much energy does ChatGPT use?* — [epoch.ai](https://epoch.ai/gradient-updates/how-much-energy-does-chatgpt-use)
138
+ - Mistral AI — *Our contribution to a global environmental standard for AI* (Large 2 LCA, with ADEME/Carbone 4) — see [coverage](https://www.deeplearning.ai/the-batch/french-ai-startup-discloses-full-lifecycle-consumption-and-emissions-for-mistral-large-2)
139
+ - Macknick et al. — water consumption factors for electricity generation (NREL)
140
+ - Provider sustainability reports: AWS water positive updates (WUE 0.15–0.18), Microsoft (~0.49), Google (fleet WUE ~1.1, PUE 1.10)
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.5
2
+ Name: soif-llm
3
+ Version: 0.1.0
4
+ Summary: Estimate the water footprint of LLM prompts, the way you estimate their cost.
5
+ Project-URL: Homepage, https://github.com/Unchained-Labs/soif
6
+ Project-URL: Repository, https://github.com/Unchained-Labs/soif
7
+ Project-URL: Issues, https://github.com/Unchained-Labs/soif/issues
8
+ Author-email: Erwin Lejeune <erwin.lejeune15@gmail.com>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Unchained Labs
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: environment,footprint,green-ai,llm,sustainability,water,wue
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: Scientific/Engineering
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.10
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=8; extra == 'dev'
47
+ Requires-Dist: ruff>=0.6; extra == 'dev'
48
+ Provides-Extra: tokenizers
49
+ Requires-Dist: tiktoken>=0.7; extra == 'tokenizers'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # soif 💧
53
+
54
+ > *soif* — French for **thirst**.
55
+
56
+ **Estimate the water footprint of LLM prompts, the way you estimate their cost.**
57
+
58
+ Every LLM answer evaporates real freshwater: data-center cooling towers (on-site) and the
59
+ power plants feeding them (off-site) both consume it. Published per-prompt figures span two
60
+ orders of magnitude — Google measured **0.26 mL** per median Gemini prompt, while Mistral's
61
+ lifecycle analysis reports **45 mL** per 400-token Large 2 response. `soif` turns model +
62
+ tokens + hosting assumptions into an honest **low / mid / high** water estimate with a fully
63
+ documented, versioned methodology ([METHODOLOGY.md](METHODOLOGY.md)).
64
+
65
+ - Pure Python, **zero runtime dependencies**, MIT-licensed.
66
+ - Library API, CLI, SDK response adapters (OpenAI / Anthropic), a Claude Code hook, and
67
+ water-aware model routing for agent graphs.
68
+
69
+ 📚 **Docs: [unchained-labs.github.io/soif](https://unchained-labs.github.io/soif/)**
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install soif-llm # imports as `soif`; from a checkout: pip install .
75
+ pip install "soif-llm[tokenizers]" # optional: exact token counts via tiktoken
76
+ ```
77
+
78
+ (The PyPI distribution is `soif-llm` — the bare name was taken — but the module is `soif`.)
79
+
80
+ ## Quick start
81
+
82
+ ```python
83
+ import soif
84
+
85
+ est = soif.estimate("gpt-4o", prompt="Explain retrieval-augmented generation.")
86
+ print(est.humanize())
87
+ # ~1.15 mL of water (23.0 drops); range 0.113 mL - 12.46 mL
88
+
89
+ est.total_ml.mid # millilitres, mid scenario
90
+ est.onsite_ml # cooling-tower evaporation at the data center
91
+ est.offsite_ml # water consumed generating the electricity
92
+ est.embodied_ml # amortised manufacturing (chips, servers, buildings)
93
+ est.assumptions # every default the estimate leaned on, spelled out
94
+ ```
95
+
96
+ The **accurate path** is to feed real token usage from an API response — this captures
97
+ actual output length, reasoning ("thinking") tokens, and cache hits:
98
+
99
+ ```python
100
+ response = client.chat.completions.create(...) # OpenAI — or Anthropic messages.create
101
+ est = soif.from_response(response)
102
+ ```
103
+
104
+ Reasoning models drink more — thinking tokens are output tokens:
105
+
106
+ ```python
107
+ soif.estimate("gpt-5", output_tokens=500, reasoning_effort="high")
108
+ soif.estimate("o3", output_tokens=500, reasoning_tokens=8000) # from real usage
109
+ ```
110
+
111
+ Control the hosting scenario:
112
+
113
+ ```python
114
+ soif.estimate("llama-3.1-70b", output_tokens=500,
115
+ provider="aws", region="nordics", # presets
116
+ include_embodied=False) # operational water only
117
+ soif.estimate("my-fine-tune", active_params_b=8, wue=0.2, pue=1.12, ewif=0.4)
118
+ ```
119
+
120
+ ## CLI
121
+
122
+ ```bash
123
+ soif estimate "why is the sky blue?" --model claude-sonnet-4-5
124
+ soif estimate -m gpt-4o -i 1200 -o 500 --json
125
+ soif compare gpt-4o gpt-4o-mini gemini-2.5-flash claude-haiku-4-5 -o 500
126
+ soif models
127
+ ```
128
+
129
+ ## Agent graphs: metering and minimising water across a chain
130
+
131
+ Two primitives make water a first-class optimization target in agentic pipelines
132
+ (LangGraph, hand-rolled DAGs, anything):
133
+
134
+ **1. `Meter` — accumulate across nodes, with a soft budget:**
135
+
136
+ ```python
137
+ meter = soif.Meter(budget_ml=50)
138
+
139
+ def summarize_node(state):
140
+ resp = client.chat.completions.create(model=state["model"], ...)
141
+ meter.record(soif.from_response(resp))
142
+ if meter.over_budget:
143
+ state["model"] = "gpt-4o-mini" # degrade later hops
144
+ return state
145
+
146
+ print(meter.summary())
147
+ # 7 call(s): ~18.2 mL of water (3.7 teaspoons); range ... — within budget (18.2/50.0 mL)
148
+ ```
149
+
150
+ **2. `soif.optimize` — route each node to the least-thirsty capable model:**
151
+
152
+ ```python
153
+ from soif import optimize
154
+
155
+ optimize.pick_model(
156
+ ["gpt-4o", "gpt-4o-mini", "gemini-2.5-flash", "claude-sonnet-4-5"],
157
+ min_tier="small", # capability floor for this node
158
+ input_tokens=2000, output_tokens=300,
159
+ )
160
+ # -> 'gemini-2.5-flash'... whichever mid-scenario estimate is lowest
161
+
162
+ optimize.savings("claude-opus-4", "claude-haiku-4-5", output_tokens=500)
163
+ # {'baseline_ml': ..., 'alternative_ml': ..., 'saved_ml': ..., 'saved_pct': ...}
164
+ ```
165
+
166
+ Model choice is the big lever (~30× between tiers); after that: shorter outputs, prompt
167
+ caching, modest reasoning effort, and low-water regions/providers.
168
+
169
+ ## Claude Code hook
170
+
171
+ Get a water read-out for every session, computed from the transcript's *real* token usage
172
+ — see [integrations/claude-code](integrations/claude-code/README.md). In short, add to
173
+ `.claude/settings.json`:
174
+
175
+ ```json
176
+ {
177
+ "hooks": {
178
+ "Stop": [
179
+ { "hooks": [{ "type": "command", "command": "soif claude-hook" }] }
180
+ ]
181
+ }
182
+ }
183
+ ```
184
+
185
+ After each turn: `soif: this session used ~4.31 mL of water (0.9 teaspoons); range ... across 12 model call(s).`
186
+
187
+ ## How it works (short version)
188
+
189
+ ```
190
+ E_it = tokens × Wh-per-token(model tier) # server energy
191
+ E_facility = E_it × PUE # + cooling/power overhead
192
+ W_onsite = E_it × WUE # cooling evaporation
193
+ W_offsite = E_facility × EWIF # power-plant water
194
+ W_total = (W_onsite + W_offsite) × lifecycle # + embodied (optional)
195
+ ```
196
+
197
+ Every factor is a *(low, mid, high)* triple propagated end-to-end, so the range reflects
198
+ genuine uncertainty rather than false precision. Factors are versioned
199
+ (`soif.factors.FACTORS_VERSION`) and calibrated against Google's measured Gemini numbers,
200
+ Epoch AI's GPT-4o analysis, Mistral's Large 2 LCA, and Ren et al.'s methodology.
201
+ **Read [METHODOLOGY.md](METHODOLOGY.md) before quoting numbers** — these are estimates,
202
+ not measurements.
203
+
204
+ ## Contributing
205
+
206
+ Factor updates (new disclosures, better WUE/PUE/EWIF data, new models) are the most
207
+ valuable contributions — please include sources. `pip install -e ".[dev]" && pytest && ruff check .`
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,160 @@
1
+ # soif 💧
2
+
3
+ > *soif* — French for **thirst**.
4
+
5
+ **Estimate the water footprint of LLM prompts, the way you estimate their cost.**
6
+
7
+ Every LLM answer evaporates real freshwater: data-center cooling towers (on-site) and the
8
+ power plants feeding them (off-site) both consume it. Published per-prompt figures span two
9
+ orders of magnitude — Google measured **0.26 mL** per median Gemini prompt, while Mistral's
10
+ lifecycle analysis reports **45 mL** per 400-token Large 2 response. `soif` turns model +
11
+ tokens + hosting assumptions into an honest **low / mid / high** water estimate with a fully
12
+ documented, versioned methodology ([METHODOLOGY.md](METHODOLOGY.md)).
13
+
14
+ - Pure Python, **zero runtime dependencies**, MIT-licensed.
15
+ - Library API, CLI, SDK response adapters (OpenAI / Anthropic), a Claude Code hook, and
16
+ water-aware model routing for agent graphs.
17
+
18
+ 📚 **Docs: [unchained-labs.github.io/soif](https://unchained-labs.github.io/soif/)**
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install soif-llm # imports as `soif`; from a checkout: pip install .
24
+ pip install "soif-llm[tokenizers]" # optional: exact token counts via tiktoken
25
+ ```
26
+
27
+ (The PyPI distribution is `soif-llm` — the bare name was taken — but the module is `soif`.)
28
+
29
+ ## Quick start
30
+
31
+ ```python
32
+ import soif
33
+
34
+ est = soif.estimate("gpt-4o", prompt="Explain retrieval-augmented generation.")
35
+ print(est.humanize())
36
+ # ~1.15 mL of water (23.0 drops); range 0.113 mL - 12.46 mL
37
+
38
+ est.total_ml.mid # millilitres, mid scenario
39
+ est.onsite_ml # cooling-tower evaporation at the data center
40
+ est.offsite_ml # water consumed generating the electricity
41
+ est.embodied_ml # amortised manufacturing (chips, servers, buildings)
42
+ est.assumptions # every default the estimate leaned on, spelled out
43
+ ```
44
+
45
+ The **accurate path** is to feed real token usage from an API response — this captures
46
+ actual output length, reasoning ("thinking") tokens, and cache hits:
47
+
48
+ ```python
49
+ response = client.chat.completions.create(...) # OpenAI — or Anthropic messages.create
50
+ est = soif.from_response(response)
51
+ ```
52
+
53
+ Reasoning models drink more — thinking tokens are output tokens:
54
+
55
+ ```python
56
+ soif.estimate("gpt-5", output_tokens=500, reasoning_effort="high")
57
+ soif.estimate("o3", output_tokens=500, reasoning_tokens=8000) # from real usage
58
+ ```
59
+
60
+ Control the hosting scenario:
61
+
62
+ ```python
63
+ soif.estimate("llama-3.1-70b", output_tokens=500,
64
+ provider="aws", region="nordics", # presets
65
+ include_embodied=False) # operational water only
66
+ soif.estimate("my-fine-tune", active_params_b=8, wue=0.2, pue=1.12, ewif=0.4)
67
+ ```
68
+
69
+ ## CLI
70
+
71
+ ```bash
72
+ soif estimate "why is the sky blue?" --model claude-sonnet-4-5
73
+ soif estimate -m gpt-4o -i 1200 -o 500 --json
74
+ soif compare gpt-4o gpt-4o-mini gemini-2.5-flash claude-haiku-4-5 -o 500
75
+ soif models
76
+ ```
77
+
78
+ ## Agent graphs: metering and minimising water across a chain
79
+
80
+ Two primitives make water a first-class optimization target in agentic pipelines
81
+ (LangGraph, hand-rolled DAGs, anything):
82
+
83
+ **1. `Meter` — accumulate across nodes, with a soft budget:**
84
+
85
+ ```python
86
+ meter = soif.Meter(budget_ml=50)
87
+
88
+ def summarize_node(state):
89
+ resp = client.chat.completions.create(model=state["model"], ...)
90
+ meter.record(soif.from_response(resp))
91
+ if meter.over_budget:
92
+ state["model"] = "gpt-4o-mini" # degrade later hops
93
+ return state
94
+
95
+ print(meter.summary())
96
+ # 7 call(s): ~18.2 mL of water (3.7 teaspoons); range ... — within budget (18.2/50.0 mL)
97
+ ```
98
+
99
+ **2. `soif.optimize` — route each node to the least-thirsty capable model:**
100
+
101
+ ```python
102
+ from soif import optimize
103
+
104
+ optimize.pick_model(
105
+ ["gpt-4o", "gpt-4o-mini", "gemini-2.5-flash", "claude-sonnet-4-5"],
106
+ min_tier="small", # capability floor for this node
107
+ input_tokens=2000, output_tokens=300,
108
+ )
109
+ # -> 'gemini-2.5-flash'... whichever mid-scenario estimate is lowest
110
+
111
+ optimize.savings("claude-opus-4", "claude-haiku-4-5", output_tokens=500)
112
+ # {'baseline_ml': ..., 'alternative_ml': ..., 'saved_ml': ..., 'saved_pct': ...}
113
+ ```
114
+
115
+ Model choice is the big lever (~30× between tiers); after that: shorter outputs, prompt
116
+ caching, modest reasoning effort, and low-water regions/providers.
117
+
118
+ ## Claude Code hook
119
+
120
+ Get a water read-out for every session, computed from the transcript's *real* token usage
121
+ — see [integrations/claude-code](integrations/claude-code/README.md). In short, add to
122
+ `.claude/settings.json`:
123
+
124
+ ```json
125
+ {
126
+ "hooks": {
127
+ "Stop": [
128
+ { "hooks": [{ "type": "command", "command": "soif claude-hook" }] }
129
+ ]
130
+ }
131
+ }
132
+ ```
133
+
134
+ After each turn: `soif: this session used ~4.31 mL of water (0.9 teaspoons); range ... across 12 model call(s).`
135
+
136
+ ## How it works (short version)
137
+
138
+ ```
139
+ E_it = tokens × Wh-per-token(model tier) # server energy
140
+ E_facility = E_it × PUE # + cooling/power overhead
141
+ W_onsite = E_it × WUE # cooling evaporation
142
+ W_offsite = E_facility × EWIF # power-plant water
143
+ W_total = (W_onsite + W_offsite) × lifecycle # + embodied (optional)
144
+ ```
145
+
146
+ Every factor is a *(low, mid, high)* triple propagated end-to-end, so the range reflects
147
+ genuine uncertainty rather than false precision. Factors are versioned
148
+ (`soif.factors.FACTORS_VERSION`) and calibrated against Google's measured Gemini numbers,
149
+ Epoch AI's GPT-4o analysis, Mistral's Large 2 LCA, and Ren et al.'s methodology.
150
+ **Read [METHODOLOGY.md](METHODOLOGY.md) before quoting numbers** — these are estimates,
151
+ not measurements.
152
+
153
+ ## Contributing
154
+
155
+ Factor updates (new disclosures, better WUE/PUE/EWIF data, new models) are the most
156
+ valuable contributions — please include sources. `pip install -e ".[dev]" && pytest && ruff check .`
157
+
158
+ ## License
159
+
160
+ MIT