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.
- soif_llm-0.1.0/.github/workflows/ci.yml +24 -0
- soif_llm-0.1.0/.github/workflows/docs.yml +51 -0
- soif_llm-0.1.0/.github/workflows/release.yml +48 -0
- soif_llm-0.1.0/.gitignore +9 -0
- soif_llm-0.1.0/LICENSE +21 -0
- soif_llm-0.1.0/METHODOLOGY.md +140 -0
- soif_llm-0.1.0/PKG-INFO +211 -0
- soif_llm-0.1.0/README.md +160 -0
- soif_llm-0.1.0/docs/agent-graphs.md +54 -0
- soif_llm-0.1.0/docs/api.md +85 -0
- soif_llm-0.1.0/docs/index.md +70 -0
- soif_llm-0.1.0/examples/agent_graph.py +42 -0
- soif_llm-0.1.0/integrations/claude-code/README.md +48 -0
- soif_llm-0.1.0/mkdocs.yml +39 -0
- soif_llm-0.1.0/pyproject.toml +61 -0
- soif_llm-0.1.0/src/soif/__init__.py +43 -0
- soif_llm-0.1.0/src/soif/_triple.py +51 -0
- soif_llm-0.1.0/src/soif/adapters.py +88 -0
- soif_llm-0.1.0/src/soif/cli.py +227 -0
- soif_llm-0.1.0/src/soif/estimator.py +263 -0
- soif_llm-0.1.0/src/soif/factors.py +124 -0
- soif_llm-0.1.0/src/soif/meter.py +76 -0
- soif_llm-0.1.0/src/soif/optimize.py +69 -0
- soif_llm-0.1.0/src/soif/py.typed +0 -0
- soif_llm-0.1.0/src/soif/registry.py +114 -0
- soif_llm-0.1.0/src/soif/tokens.py +29 -0
- soif_llm-0.1.0/tests/test_adapters.py +57 -0
- soif_llm-0.1.0/tests/test_cli.py +75 -0
- soif_llm-0.1.0/tests/test_estimator.py +112 -0
- soif_llm-0.1.0/tests/test_meter_optimize.py +40 -0
- soif_llm-0.1.0/tests/test_tokens_registry.py +24 -0
|
@@ -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
|
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)
|
soif_llm-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
soif_llm-0.1.0/README.md
ADDED
|
@@ -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
|