greenpick 0.6.2__tar.gz → 0.7.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.
- {greenpick-0.6.2/src/greenpick.egg-info → greenpick-0.7.0}/PKG-INFO +7 -4
- {greenpick-0.6.2 → greenpick-0.7.0}/README.md +6 -3
- {greenpick-0.6.2 → greenpick-0.7.0}/pyproject.toml +1 -1
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/__init__.py +1 -1
- greenpick-0.7.0/src/greenpick/data/power.json +21 -0
- greenpick-0.7.0/src/greenpick/data/pricing.json +65 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/server.py +57 -9
- greenpick-0.7.0/src/greenpick/services/carbon_service.py +51 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/data_hint.py +4 -2
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/justification_service.py +5 -1
- greenpick-0.7.0/src/greenpick/services/serverless_service.py +144 -0
- greenpick-0.7.0/src/greenpick/services/sources.py +109 -0
- {greenpick-0.6.2 → greenpick-0.7.0/src/greenpick.egg-info}/PKG-INFO +7 -4
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/SOURCES.txt +2 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/tests/test_services.py +125 -2
- greenpick-0.6.2/src/greenpick/data/power.json +0 -7
- greenpick-0.6.2/src/greenpick/data/pricing.json +0 -31
- greenpick-0.6.2/src/greenpick/services/carbon_service.py +0 -28
- {greenpick-0.6.2 → greenpick-0.7.0}/LICENSE +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/setup.cfg +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/__main__.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/config.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/annual.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/network.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/models.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/migration_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/offhours_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/typical_ranking_service.py +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
|
|
5
5
|
Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,7 +44,7 @@ Dynamic: license-file
|
|
|
44
44
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
45
45
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
46
46
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
47
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
47
|
+
<img src="https://img.shields.io/badge/MCP%20tools-20-0ea5e9" alt="MCP tools: 20" />
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -52,7 +52,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
52
52
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
53
53
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
54
54
|
|
|
55
|
-
> **Status: working —
|
|
55
|
+
> **Status: working — 20 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
56
56
|
> IESO — no key needed), cross-cloud cost
|
|
57
57
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
58
58
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -143,7 +143,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
143
143
|
|
|
144
144
|
## MCP tools
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
20 tools, grouped by what you're trying to do.
|
|
147
147
|
|
|
148
148
|
### Pick a region
|
|
149
149
|
|
|
@@ -176,6 +176,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
176
176
|
|------|--------------|
|
|
177
177
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
178
178
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
179
|
+
| `serverless_check` | For a mostly idle or spiky service: an always-on server vs. paying per request (AWS Lambda, Cloud Run). Monthly cost of each, the break-even request count, and estimated carbon (serverless as a lower bound). |
|
|
179
180
|
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
180
181
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
181
182
|
| `migration_payoff` | Does moving the **data** to the compute's region pay off? Payback in months, one-off vs monthly egress and transfer carbon, net over 12 months. Egress only. |
|
|
@@ -226,6 +227,8 @@ flow-traced consumption. Cost is a ballpark, not a quote. Latency is a geographi
|
|
|
226
227
|
|
|
227
228
|
## Quick start
|
|
228
229
|
|
|
230
|
+
<a href="https://mcprush.com/bithug01/greenpick-mcp"><img src="https://mcprush.com/badge/greenpick-mcp/card.svg" width="220" alt="GreenPick MCP server on mcprush" /></a>
|
|
231
|
+
|
|
229
232
|
```bash
|
|
230
233
|
# Python 3.11+
|
|
231
234
|
pip install greenpick # from PyPI
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
15
15
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
16
16
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
17
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
17
|
+
<img src="https://img.shields.io/badge/MCP%20tools-20-0ea5e9" alt="MCP tools: 20" />
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -22,7 +22,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
22
22
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
23
23
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
24
24
|
|
|
25
|
-
> **Status: working —
|
|
25
|
+
> **Status: working — 20 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
26
26
|
> IESO — no key needed), cross-cloud cost
|
|
27
27
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
28
28
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -113,7 +113,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
113
113
|
|
|
114
114
|
## MCP tools
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
20 tools, grouped by what you're trying to do.
|
|
117
117
|
|
|
118
118
|
### Pick a region
|
|
119
119
|
|
|
@@ -146,6 +146,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
146
146
|
|------|--------------|
|
|
147
147
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
148
148
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
149
|
+
| `serverless_check` | For a mostly idle or spiky service: an always-on server vs. paying per request (AWS Lambda, Cloud Run). Monthly cost of each, the break-even request count, and estimated carbon (serverless as a lower bound). |
|
|
149
150
|
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
150
151
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
151
152
|
| `migration_payoff` | Does moving the **data** to the compute's region pay off? Payback in months, one-off vs monthly egress and transfer carbon, net over 12 months. Egress only. |
|
|
@@ -196,6 +197,8 @@ flow-traced consumption. Cost is a ballpark, not a quote. Latency is a geographi
|
|
|
196
197
|
|
|
197
198
|
## Quick start
|
|
198
199
|
|
|
200
|
+
<a href="https://mcprush.com/bithug01/greenpick-mcp"><img src="https://mcprush.com/badge/greenpick-mcp/card.svg" width="220" alt="GreenPick MCP server on mcprush" /></a>
|
|
201
|
+
|
|
199
202
|
```bash
|
|
200
203
|
# Python 3.11+
|
|
201
204
|
pip install greenpick # from PyPI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "greenpick"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -4,7 +4,7 @@ Greener cloud-region choices with honest carbon + cost + latency tradeoffs,
|
|
|
4
4
|
delivered inside the vibe-coding loop. Directional guidance, not audited accounting.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.7.0"
|
|
8
8
|
|
|
9
9
|
# httpx logs every request URL at INFO, and our live sources pass their API key / token as a URL
|
|
10
10
|
# query parameter — so at the default log level the secret would land in the MCP client's logs.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Simple, documented power model for the carbon ESTIMATE. Turns a workload (vCPU, RAM) into average watts, then kWh/month. These are deliberately rough published-style coefficients; this is an estimate, not a measurement. PUE is the datacenter overhead multiplier.",
|
|
3
|
+
"watts_per_vcpu": 3.5,
|
|
4
|
+
"watts_per_gb_ram": 0.4,
|
|
5
|
+
"pue_default": 1.2,
|
|
6
|
+
"_sources": "Coefficients are order-of-magnitude figures consistent with public cloud power-model literature (e.g. Cloud Carbon Footprint methodology). Replace with cited values before any non-directional use.",
|
|
7
|
+
"_utilization_comment": "Used only when a caller gives a CPU utilisation. Per-vCPU power scales from an idle minimum to a full-load maximum: watts = min + utilisation x (max - min) (the Cloud Carbon Footprint / Etsy Cloud Jewels method). Without a utilisation GreenPick keeps the flat watts_per_vcpu above, which equals the AWS full-load figure, so it is a conservative (high) estimate. RAM watts do not change with load.",
|
|
8
|
+
"min_watts_per_vcpu": {
|
|
9
|
+
"aws": 0.74,
|
|
10
|
+
"gcp": 0.71,
|
|
11
|
+
"azure": 0.78
|
|
12
|
+
},
|
|
13
|
+
"max_watts_per_vcpu": {
|
|
14
|
+
"aws": 3.5,
|
|
15
|
+
"gcp": 4.26,
|
|
16
|
+
"azure": 3.76
|
|
17
|
+
},
|
|
18
|
+
"serverless_assumed_utilization": 0.5,
|
|
19
|
+
"serverless_mb_per_vcpu": 1792,
|
|
20
|
+
"_utilization_sources": "Cloud Carbon Footprint methodology, average min/max watts per vCPU by provider from SPECpower (https://www.cloudcarbonfootprint.org/docs/methodology/), checked 2026-11-10. Serverless: CCF's Lambda approach (1,792 MB = 1 vCPU; 50% average utilisation for hyperscale servers)."
|
|
21
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "DATED, STATIC pricing ballpark for v1. Deterministic — the LLM must never invent these. Figures are rough on-demand approximations for a general-purpose vCPU+RAM service and per-GB internet egress; refresh periodically and update _as_of. NOT a quote.",
|
|
3
|
+
"_as_of": "2026-10-12",
|
|
4
|
+
"_units": {
|
|
5
|
+
"compute": "USD per vCPU-hour and USD per GB-RAM-hour",
|
|
6
|
+
"egress": "USD per GB to internet"
|
|
7
|
+
},
|
|
8
|
+
"_egress_comment": "Egress is NOT one price — it's a tier ladder by WHERE the bytes cross. We price the locality tier (same-region / cross-region / cross-cloud+internet) as a LOW-HIGH RANGE because region-pair rates vary. Per-GB, USD, DIRECTIONAL list prices as of _as_of; excludes free-tier allowances, committed-use/negotiated discounts, and NAT/load-balancer processing fees. Not a quote. Keys: internet rate stays per-provider (differs most); same-region & cross-region tiers are broadly consistent across AWS/GCP/Azure so we use one shared range.",
|
|
9
|
+
"_egress_tiers_usd_per_gb": {
|
|
10
|
+
"same_region": {
|
|
11
|
+
"low": 0.0,
|
|
12
|
+
"high": 0.01,
|
|
13
|
+
"note": "Same region: free within an AZ over private IP; ~$0.01/GB each way if it crosses AZs."
|
|
14
|
+
},
|
|
15
|
+
"cross_region_same_cloud": {
|
|
16
|
+
"low": 0.01,
|
|
17
|
+
"high": 0.02,
|
|
18
|
+
"note": "Inter-region within one cloud: most pairs ~$0.01-0.02/GB; a few source regions/continent pairs run higher."
|
|
19
|
+
},
|
|
20
|
+
"cross_cloud": {
|
|
21
|
+
"low": 0.085,
|
|
22
|
+
"high": 0.12,
|
|
23
|
+
"note": "Leaving the cloud to another provider or the public internet: the expensive tier; GCP Premium ~$0.12, AWS ~$0.09, Azure ~$0.087 at entry bands."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"_egress_sources": "AWS/GCP/Azure published data-transfer pricing pages + corroborating 2026 guides (usage.ai, nops.io, egresscost.com, redresscompliance, bacancy, tatacommunications). Rounded to ranges; verify against the provider pricing page for a specific region pair.",
|
|
27
|
+
"aws": {
|
|
28
|
+
"compute_usd_per_vcpu_hour": 0.0469,
|
|
29
|
+
"compute_usd_per_gb_ram_hour": 0.0039,
|
|
30
|
+
"egress_usd_per_gb": 0.09
|
|
31
|
+
},
|
|
32
|
+
"gcp": {
|
|
33
|
+
"compute_usd_per_vcpu_hour": 0.0438,
|
|
34
|
+
"compute_usd_per_gb_ram_hour": 0.0037,
|
|
35
|
+
"egress_usd_per_gb": 0.085
|
|
36
|
+
},
|
|
37
|
+
"azure": {
|
|
38
|
+
"compute_usd_per_vcpu_hour": 0.0472,
|
|
39
|
+
"compute_usd_per_gb_ram_hour": 0.0039,
|
|
40
|
+
"egress_usd_per_gb": 0.087
|
|
41
|
+
},
|
|
42
|
+
"_source": "Compute coefficients for [aws+azure+gcp] derived from Vantage live data using ONE matched method: general-purpose 4GB/vCPU families (AWS m5/m6i, GCP n2-standard, Azure D-series v5), on-demand Linux, mean $/vCPU across regions, 25% split to RAM. Egress remains static. Apples-to-apples DIRECTIONAL comparison, not a quote.",
|
|
43
|
+
"_serverless_usd": {
|
|
44
|
+
"_as_of": "2026-11-10",
|
|
45
|
+
"_comment": "On-demand list prices for request-driven serverless compute, before free tiers and discounts. DIRECTIONAL, not a quote. Azure Functions prices are not bundled (the public page renders prices client-side and could not be verified).",
|
|
46
|
+
"aws": {
|
|
47
|
+
"product": "AWS Lambda (event functions, x86, first tier)",
|
|
48
|
+
"usd_per_gb_second": 1.66667e-05,
|
|
49
|
+
"usd_per_million_requests": 0.2,
|
|
50
|
+
"free_requests": 1000000,
|
|
51
|
+
"free_gb_seconds": 400000,
|
|
52
|
+
"source": "https://aws.amazon.com/lambda/pricing/"
|
|
53
|
+
},
|
|
54
|
+
"gcp": {
|
|
55
|
+
"product": "Cloud Run (request-based billing, tier 1)",
|
|
56
|
+
"usd_per_vcpu_second": 2.4e-05,
|
|
57
|
+
"usd_per_gib_second": 2.5e-06,
|
|
58
|
+
"usd_per_million_requests": 0.4,
|
|
59
|
+
"free_requests": 2000000,
|
|
60
|
+
"free_vcpu_seconds": 180000,
|
|
61
|
+
"free_gib_seconds": 360000,
|
|
62
|
+
"source": "https://cloud.google.com/run/pricing"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""MCP server: exposes GreenPick's
|
|
1
|
+
"""MCP server: exposes GreenPick's 20 tools over FastMCP.
|
|
2
2
|
|
|
3
3
|
Thin layer — parse/validate inputs, call services, return JSON-friendly dicts. All math
|
|
4
4
|
lives in services/; this file only wires tools to them.
|
|
@@ -26,10 +26,11 @@ from .services import (
|
|
|
26
26
|
carbon_service, recommender, scheduling_service, instance_service, locality_service,
|
|
27
27
|
training_service, forecast_service, serving_service, metric_scaffold_service,
|
|
28
28
|
rightsizing_service, inference_service, offhours_service, typical_ranking_service,
|
|
29
|
-
justification_service, migration_service,
|
|
29
|
+
justification_service, migration_service, serverless_service,
|
|
30
30
|
)
|
|
31
31
|
from .models import ServingHub
|
|
32
32
|
from .services.data_hint import with_hint
|
|
33
|
+
from .services.sources import with_sources
|
|
33
34
|
from .data.typical_repository import typical_context
|
|
34
35
|
|
|
35
36
|
# Sent to every MCP client at connect. Agents learn a server from its tool list, not its README, and
|
|
@@ -40,7 +41,8 @@ GreenPick advises on cloud infrastructure decisions BEFORE you deploy, weighing
|
|
|
40
41
|
latency together. Strongest uses, in order:
|
|
41
42
|
1. Right-sizing (rightsizing_check): an over-provisioned instance cut to a standard, buyable size \
|
|
42
43
|
saves money and carbon in the same move. Lead with the monthly dollar saving. For dev, test or other \
|
|
43
|
-
environments that don't need to run 24/7, offhours_check shows what switching them off saves.
|
|
44
|
+
environments that don't need to run 24/7, offhours_check shows what switching them off saves. For a \
|
|
45
|
+
mostly idle or spiky service, serverless_check compares an always-on server with paying per request.
|
|
44
46
|
2. Scheduling deferrable jobs: cleanest_hours_to_run (one-off), cleanest_time_of_day (recurring), \
|
|
45
47
|
carbon_forecast (EU day-ahead).
|
|
46
48
|
3. GPU training and LLM inference footprints: estimate_training_carbon (gpu_hours is the TOTAL across \
|
|
@@ -54,7 +56,8 @@ for a pull request, use region_move_justification.
|
|
|
54
56
|
Every carbon figure carries source and quality fields. Report 'typical', 'mock', 'cached' or \
|
|
55
57
|
'historical' values as such, never as live readings — including figures derived from them, such as a kg CO2 saving. \
|
|
56
58
|
When a result includes live_data_hint, pass it on: it says why the data isn't live and how to turn \
|
|
57
|
-
live data on.
|
|
59
|
+
live data on. Results with carbon or cost figures include verify_at: the public page behind each \
|
|
60
|
+
figure, for anyone who wants to check it. If a call fails or output is missing, say so rather than describing \
|
|
58
61
|
what the tool would return. GreenPick estimates; it does not measure running systems or report model \
|
|
59
62
|
accuracy. Use explain_methodology_and_limits when asked how a number was produced.\
|
|
60
63
|
"""
|
|
@@ -146,13 +149,19 @@ async def estimate_workload_carbon(
|
|
|
146
149
|
vcpu: float,
|
|
147
150
|
ram_gb: float,
|
|
148
151
|
hours_per_month: float = 730,
|
|
152
|
+
avg_cpu_utilization: Optional[float] = None,
|
|
149
153
|
) -> dict:
|
|
150
154
|
"""Rough kgCO2/month for a workload running in a region. An ESTIMATE (documented power
|
|
151
|
-
model x grid intensity), not a measurement.
|
|
155
|
+
model x grid intensity), not a measurement. Without avg_cpu_utilization it assumes the CPU is
|
|
156
|
+
fully busy (a conservative, high figure). Pass the observed average CPU utilisation as a fraction
|
|
157
|
+
(0.1 for 10%) for a more realistic figure: an idle server draws far less than a busy one."""
|
|
158
|
+
err = _fraction_error("avg_cpu_utilization", avg_cpu_utilization, allow_zero=True)
|
|
159
|
+
if err:
|
|
160
|
+
return {"provider": provider, "region": region, "error": err}
|
|
152
161
|
reading = await carbon_service.carbon_now(_repo, provider, region) # type: ignore[arg-type]
|
|
153
162
|
wl = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=hours_per_month)
|
|
154
163
|
kg = carbon_service.estimate_kg_co2_per_month(wl, reading)
|
|
155
|
-
|
|
164
|
+
out = {
|
|
156
165
|
"provider": provider,
|
|
157
166
|
"region": region,
|
|
158
167
|
"estimated_kg_co2_per_month": kg,
|
|
@@ -160,7 +169,15 @@ async def estimate_workload_carbon(
|
|
|
160
169
|
"carbon_source": reading.source,
|
|
161
170
|
"carbon_quality": reading.quality,
|
|
162
171
|
"note": "Estimate only: documented power model x grid intensity. Not a measurement.",
|
|
163
|
-
}
|
|
172
|
+
}
|
|
173
|
+
if avg_cpu_utilization is not None:
|
|
174
|
+
out["estimated_kg_co2_per_month_at_full_load"] = kg
|
|
175
|
+
out["estimated_kg_co2_per_month"] = carbon_service.estimate_kg_co2_per_month_at(
|
|
176
|
+
wl, reading, avg_cpu_utilization)
|
|
177
|
+
out["avg_cpu_utilization"] = avg_cpu_utilization
|
|
178
|
+
out["note"] = ("Estimate only. CPU power follows the utilisation you gave (Cloud Carbon Footprint "
|
|
179
|
+
"min/max watts per vCPU); RAM power does not change with load. Not a measurement.")
|
|
180
|
+
return with_hint(out)
|
|
164
181
|
|
|
165
182
|
|
|
166
183
|
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
@@ -271,6 +288,37 @@ async def rightsizing_check(
|
|
|
271
288
|
return with_hint(advice.model_dump())
|
|
272
289
|
|
|
273
290
|
|
|
291
|
+
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
292
|
+
async def serverless_check(
|
|
293
|
+
provider: str,
|
|
294
|
+
region: str,
|
|
295
|
+
vcpu: float,
|
|
296
|
+
ram_gb: float,
|
|
297
|
+
avg_cpu_utilization: float,
|
|
298
|
+
monthly_requests: float,
|
|
299
|
+
avg_duration_ms: float,
|
|
300
|
+
function_memory_gb: float = 1.0,
|
|
301
|
+
) -> dict:
|
|
302
|
+
"""Would serverless change the math? Compares an always-on server (its size and observed average CPU
|
|
303
|
+
utilisation as a fraction, 0.05 for 5%) with a request-driven serverless platform (AWS Lambda or
|
|
304
|
+
Google Cloud Run) for the same traffic: monthly requests, average duration per request in ms, and the
|
|
305
|
+
function's memory in GB. Returns the monthly cost of each, the request count where they break even,
|
|
306
|
+
and estimated carbon for each. Lead with the dollar figure. Serverless carbon is a LOWER BOUND
|
|
307
|
+
(platform overhead isn't published); cost is list price before free tiers. Azure serverless cost is
|
|
308
|
+
not priced (carbon only). Ask the user for traffic figures rather than assuming them."""
|
|
309
|
+
err = _fraction_error("avg_cpu_utilization", avg_cpu_utilization, allow_zero=True)
|
|
310
|
+
if err:
|
|
311
|
+
return {"provider": provider, "region": region, "error": err}
|
|
312
|
+
if monthly_requests < 0 or avg_duration_ms <= 0 or not 0.125 <= function_memory_gb <= 10.24:
|
|
313
|
+
return {"provider": provider, "region": region,
|
|
314
|
+
"error": "monthly_requests must be >= 0, avg_duration_ms > 0, and function_memory_gb "
|
|
315
|
+
"between 0.125 and 10.24 (Lambda's range)."}
|
|
316
|
+
return with_hint(await serverless_service.serverless_check(
|
|
317
|
+
_repo, provider, region, vcpu, ram_gb, avg_cpu_utilization,
|
|
318
|
+
monthly_requests, avg_duration_ms, function_memory_gb,
|
|
319
|
+
))
|
|
320
|
+
|
|
321
|
+
|
|
274
322
|
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
275
323
|
async def offhours_check(
|
|
276
324
|
provider: str,
|
|
@@ -308,9 +356,9 @@ def greenest_regions_on_average(
|
|
|
308
356
|
norm rather than a single hour; use region_carbon_now for what the grid is doing today. Carbon only
|
|
309
357
|
(no cost or latency; compare_regions_tri_axis weighs all three). Offline, needs no keys. Regions
|
|
310
358
|
labelled annual_average have only a yearly national figure and are not month-specific."""
|
|
311
|
-
return typical_ranking_service.rank_regions(
|
|
359
|
+
return with_sources(typical_ranking_service.rank_regions(
|
|
312
360
|
month=month, whole_year=whole_year, providers=providers, regions=regions, top_n=top_n,
|
|
313
|
-
)
|
|
361
|
+
))
|
|
314
362
|
|
|
315
363
|
|
|
316
364
|
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Carbon readings (via the repository) + a workload CO2 ESTIMATE.
|
|
2
|
+
|
|
3
|
+
The estimate turns a workload into average watts (power model) -> kWh/month ->
|
|
4
|
+
multiply by grid intensity. Clearly an estimate, not a measurement.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from ..models import CarbonReading, Workload, Provider
|
|
9
|
+
from ..data.repository import CarbonRepository
|
|
10
|
+
from ..data.static_data import power_model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def carbon_now(repo: CarbonRepository, provider: Provider, region: str) -> CarbonReading:
|
|
14
|
+
return await repo.carbon_now(provider, region)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def workload_kwh_per_month(workload: Workload) -> float:
|
|
18
|
+
pm = power_model()
|
|
19
|
+
watts = workload.vcpu * pm["watts_per_vcpu"] + workload.ram_gb * pm["watts_per_gb_ram"]
|
|
20
|
+
watts *= pm["pue_default"]
|
|
21
|
+
kwh = watts / 1000.0 * workload.hours_per_month
|
|
22
|
+
return kwh
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def estimate_kg_co2_per_month(workload: Workload, reading: CarbonReading) -> float:
|
|
26
|
+
kwh = workload_kwh_per_month(workload)
|
|
27
|
+
grams = kwh * reading.g_co2_per_kwh
|
|
28
|
+
return round(grams / 1000.0, 2)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def watts_per_vcpu_at(provider: str, cpu_utilization: float) -> float:
|
|
32
|
+
"""Average watts per vCPU at a given CPU utilisation (0..1): idle minimum plus the load share of
|
|
33
|
+
the gap to full load (Cloud Carbon Footprint method). Unknown providers use the AWS range."""
|
|
34
|
+
pm = power_model()
|
|
35
|
+
lo = pm["min_watts_per_vcpu"].get(provider, pm["min_watts_per_vcpu"]["aws"])
|
|
36
|
+
hi = pm["max_watts_per_vcpu"].get(provider, pm["max_watts_per_vcpu"]["aws"])
|
|
37
|
+
u = max(0.0, min(1.0, cpu_utilization))
|
|
38
|
+
return lo + u * (hi - lo)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def workload_kwh_per_month_at(workload: Workload, provider: str, cpu_utilization: float) -> float:
|
|
42
|
+
"""Like workload_kwh_per_month, but CPU power follows utilisation instead of assuming full load.
|
|
43
|
+
RAM power is unchanged (memory draws roughly the same whether busy or not)."""
|
|
44
|
+
pm = power_model()
|
|
45
|
+
watts = workload.vcpu * watts_per_vcpu_at(provider, cpu_utilization) + workload.ram_gb * pm["watts_per_gb_ram"]
|
|
46
|
+
return watts * pm["pue_default"] / 1000.0 * workload.hours_per_month
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def estimate_kg_co2_per_month_at(workload: Workload, reading: CarbonReading, cpu_utilization: float) -> float:
|
|
50
|
+
kwh = workload_kwh_per_month_at(workload, reading.provider, cpu_utilization)
|
|
51
|
+
return round(kwh * reading.g_co2_per_kwh / 1000.0, 2)
|
|
@@ -180,8 +180,10 @@ def build_hint(result: dict) -> Optional[str]:
|
|
|
180
180
|
|
|
181
181
|
|
|
182
182
|
def with_hint(result: dict) -> dict:
|
|
183
|
-
"""Add `live_data_hint` to a tool result that uses fallback data
|
|
183
|
+
"""Add `live_data_hint` to a tool result that uses fallback data, and `verify_at` (where to check
|
|
184
|
+
its carbon and cost figures) to any result that has them. Existing fields are left unchanged."""
|
|
185
|
+
from .sources import with_sources
|
|
184
186
|
hint = build_hint(result)
|
|
185
187
|
if hint:
|
|
186
188
|
result["live_data_hint"] = hint
|
|
187
|
-
return result
|
|
189
|
+
return with_sources(result)
|
|
@@ -122,7 +122,11 @@ async def region_move_justification(
|
|
|
122
122
|
if net_kg is not None:
|
|
123
123
|
cost_part = (f" and about ${egress_low:,.0f}-${egress_high:,.0f} a month in egress"
|
|
124
124
|
if egress_low is not None else "")
|
|
125
|
-
if
|
|
125
|
+
if kg_saving <= 0:
|
|
126
|
+
parts.append(
|
|
127
|
+
f"The data stays in {from_provider}/{from_region} (~{monthly_cross_boundary_gb:,.0f} GB a "
|
|
128
|
+
f"month crossing), which adds about {transfer_kg:.1f} kg CO2{cost_part} on top.")
|
|
129
|
+
elif net_kg > 0:
|
|
126
130
|
parts.append(
|
|
127
131
|
f"The data stays in {from_provider}/{from_region} (~{monthly_cross_boundary_gb:,.0f} GB a "
|
|
128
132
|
f"month crossing), which adds about {transfer_kg:.1f} kg CO2{cost_part}, "
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Would serverless change the math? An always-on server vs. paying only for the time code runs.
|
|
2
|
+
|
|
3
|
+
A mostly idle service on an always-on server pays (and draws power) around the clock, even when it is
|
|
4
|
+
doing nothing. A request-driven serverless platform (AWS Lambda, Google Cloud Run) charges, and keeps
|
|
5
|
+
hardware busy, only while requests run. Which is cheaper depends on how much traffic there is: low or
|
|
6
|
+
spiky traffic favours serverless, steady high traffic favours a server. This compares the two on cost
|
|
7
|
+
and on estimated carbon, and gives the monthly request count where they break even on cost.
|
|
8
|
+
|
|
9
|
+
Directional on both axes. The server side uses the utilisation-aware power model (Cloud Carbon
|
|
10
|
+
Footprint min/max watts per vCPU). The serverless side counts only the compute time requests run, at
|
|
11
|
+
the CCF Lambda assumptions (1,792 MB of memory = 1 vCPU, 50% average utilisation), so it is a LOWER
|
|
12
|
+
BOUND: platform overhead such as warm instances and control planes is not published and is not
|
|
13
|
+
included. Prices are dated on-demand list prices, before free tiers and discounts.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from ..data.repository import CarbonRepository
|
|
20
|
+
from ..data.static_data import power_model, pricing
|
|
21
|
+
from ..models import Workload
|
|
22
|
+
from . import carbon_service, cost_service
|
|
23
|
+
|
|
24
|
+
HOURS_PER_MONTH = 730
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _serverless_cost(provider: str, requests: float, active_seconds: float, memory_gb: float,
|
|
28
|
+
vcpu_equiv: float) -> tuple[Optional[float], Optional[float]]:
|
|
29
|
+
"""(cost, cost per million requests) for a month, before free tiers; (None, None) if unpriced."""
|
|
30
|
+
p = (pricing().get("_serverless_usd") or {}).get(provider)
|
|
31
|
+
if not p:
|
|
32
|
+
return None, None
|
|
33
|
+
if provider == "aws":
|
|
34
|
+
per_req_compute = (active_seconds / requests if requests else 0) * memory_gb * p["usd_per_gb_second"]
|
|
35
|
+
else: # gcp, Cloud Run request-based: vCPU-seconds + GiB-seconds while requests run
|
|
36
|
+
per_req_compute = ((active_seconds / requests if requests else 0)
|
|
37
|
+
* (vcpu_equiv * p["usd_per_vcpu_second"] + memory_gb * p["usd_per_gib_second"]))
|
|
38
|
+
per_req = per_req_compute + p["usd_per_million_requests"] / 1e6
|
|
39
|
+
return round(per_req * requests, 2), per_req * 1e6
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def serverless_check(
|
|
43
|
+
repo: CarbonRepository,
|
|
44
|
+
provider: str,
|
|
45
|
+
region: str,
|
|
46
|
+
vcpu: float,
|
|
47
|
+
ram_gb: float,
|
|
48
|
+
avg_cpu_utilization: float,
|
|
49
|
+
monthly_requests: float,
|
|
50
|
+
avg_duration_ms: float,
|
|
51
|
+
function_memory_gb: float = 1.0,
|
|
52
|
+
) -> dict:
|
|
53
|
+
pm = power_model()
|
|
54
|
+
reading = await carbon_service.carbon_now(repo, provider, region) # type: ignore[arg-type]
|
|
55
|
+
|
|
56
|
+
# Always-on server: the bill for every hour, and power that follows its real utilisation.
|
|
57
|
+
vm = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=HOURS_PER_MONTH)
|
|
58
|
+
vm_cost = cost_service.estimate_monthly_cost_usd(provider, vm) # type: ignore[arg-type]
|
|
59
|
+
vm_kg = carbon_service.estimate_kg_co2_per_month_at(vm, reading, avg_cpu_utilization)
|
|
60
|
+
vm_kg_full = carbon_service.estimate_kg_co2_per_month(vm, reading)
|
|
61
|
+
|
|
62
|
+
# Serverless: only the time requests run, with CPU allocated in proportion to memory.
|
|
63
|
+
active_seconds = monthly_requests * avg_duration_ms / 1000.0
|
|
64
|
+
active_hours = active_seconds / 3600.0
|
|
65
|
+
vcpu_equiv = min(6.0, function_memory_gb * 1024 / pm["serverless_mb_per_vcpu"])
|
|
66
|
+
sl = Workload(vcpu=max(vcpu_equiv, 1e-6), ram_gb=function_memory_gb, hours_per_month=max(active_hours, 1e-9))
|
|
67
|
+
sl_kg = (carbon_service.estimate_kg_co2_per_month_at(sl, reading, pm["serverless_assumed_utilization"])
|
|
68
|
+
if active_hours > 0 else 0.0)
|
|
69
|
+
sl_cost, per_million = _serverless_cost(provider, monthly_requests, active_seconds, function_memory_gb, vcpu_equiv)
|
|
70
|
+
|
|
71
|
+
breakeven = None
|
|
72
|
+
if vm_cost is not None and per_million:
|
|
73
|
+
breakeven = round(vm_cost / per_million * 1e6)
|
|
74
|
+
cost_saving = round(vm_cost - sl_cost, 2) if vm_cost is not None and sl_cost is not None else None
|
|
75
|
+
cost_pct = round(cost_saving / vm_cost * 100, 1) if cost_saving is not None and vm_cost else None
|
|
76
|
+
kg_saving = round(vm_kg - sl_kg, 2)
|
|
77
|
+
carbon_pct = round(kg_saving / vm_kg * 100, 1) if vm_kg > 0 else None
|
|
78
|
+
|
|
79
|
+
# Does this traffic fit inside the provider's monthly free tier? Then the real bill could be $0.
|
|
80
|
+
free = (pricing().get("_serverless_usd") or {}).get(provider) or {}
|
|
81
|
+
in_free_tier = None
|
|
82
|
+
if free:
|
|
83
|
+
req_ok = monthly_requests <= free.get("free_requests", 0)
|
|
84
|
+
if provider == "aws":
|
|
85
|
+
use_ok = active_seconds * function_memory_gb <= free.get("free_gb_seconds", 0)
|
|
86
|
+
else:
|
|
87
|
+
use_ok = (active_seconds * vcpu_equiv <= free.get("free_vcpu_seconds", 0)
|
|
88
|
+
and active_seconds * function_memory_gb <= free.get("free_gib_seconds", 0))
|
|
89
|
+
in_free_tier = bool(req_ok and use_ok)
|
|
90
|
+
|
|
91
|
+
if cost_saving is None:
|
|
92
|
+
cost_txt = ("Serverless cost isn't priced for this provider in GreenPick (check the provider's pricing "
|
|
93
|
+
"page); the comparison below is carbon only.")
|
|
94
|
+
verdict = "carbon_only"
|
|
95
|
+
elif cost_saving > 0:
|
|
96
|
+
cost_txt = (f"Serverless would cost about ${sl_cost:,.2f} a month against ${vm_cost:,.2f} for the always-on "
|
|
97
|
+
f"server: about ${cost_saving:,.0f} a month less ({cost_pct:.0f}%).")
|
|
98
|
+
verdict = "serverless_cheaper"
|
|
99
|
+
else:
|
|
100
|
+
cost_txt = (f"Serverless would cost about ${sl_cost:,.2f} a month against ${vm_cost:,.2f} for the always-on "
|
|
101
|
+
f"server: about ${-cost_saving:,.0f} a month MORE. At this traffic a server is the cheaper choice.")
|
|
102
|
+
verdict = "server_cheaper"
|
|
103
|
+
free_txt = (" This traffic fits within the provider's monthly free tier, so the actual serverless bill "
|
|
104
|
+
"could be $0." if in_free_tier else "")
|
|
105
|
+
be_txt = (f" They break even at about {breakeven:,} requests a month at this duration and memory; above that, "
|
|
106
|
+
"the server wins on cost." if breakeven else "")
|
|
107
|
+
summary = (f"{cost_txt}{free_txt}{be_txt} Estimated carbon: about {sl_kg:.2f} kg CO2 a month serverless (a lower bound) "
|
|
108
|
+
f"against {vm_kg:.2f} kg for the server at {avg_cpu_utilization * 100:.0f}% CPU use "
|
|
109
|
+
f"({reading.quality} grid data).")
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
"summary": summary,
|
|
113
|
+
"verdict": verdict,
|
|
114
|
+
"provider": provider,
|
|
115
|
+
"region": region,
|
|
116
|
+
"server": {"vcpu": vcpu, "ram_gb": ram_gb, "avg_cpu_utilization": avg_cpu_utilization,
|
|
117
|
+
"hours_per_month": HOURS_PER_MONTH},
|
|
118
|
+
"serverless": {"monthly_requests": monthly_requests, "avg_duration_ms": avg_duration_ms,
|
|
119
|
+
"function_memory_gb": function_memory_gb, "vcpu_equivalent": round(vcpu_equiv, 2),
|
|
120
|
+
"billed_hours_per_month": round(active_hours, 1)},
|
|
121
|
+
"server_cost_usd_per_month": vm_cost,
|
|
122
|
+
"serverless_cost_usd_per_month": sl_cost,
|
|
123
|
+
"monthly_usd_saving": cost_saving,
|
|
124
|
+
"cost_saving_pct": cost_pct,
|
|
125
|
+
"breakeven_monthly_requests": breakeven,
|
|
126
|
+
"within_free_tier": in_free_tier,
|
|
127
|
+
"server_kg_co2_per_month": vm_kg,
|
|
128
|
+
"server_kg_co2_per_month_at_full_load": vm_kg_full,
|
|
129
|
+
"serverless_kg_co2_per_month_lower_bound": sl_kg,
|
|
130
|
+
"monthly_kg_co2_saving": kg_saving,
|
|
131
|
+
"carbon_saving_pct": carbon_pct,
|
|
132
|
+
"carbon_source": reading.source,
|
|
133
|
+
"carbon_quality": reading.quality,
|
|
134
|
+
"limitations": (
|
|
135
|
+
"Directional. Serverless bills each request's full duration, including time spent waiting on a "
|
|
136
|
+
"database or another service, while a server handles many requests at once, so I/O-heavy services "
|
|
137
|
+
"favour a server more than these figures suggest. Serverless cost is on-demand list price before free tiers (AWS gives 1M requests and "
|
|
138
|
+
"400,000 GB-seconds a month free; Cloud Run 2M requests and some vCPU/GiB-seconds), and excludes "
|
|
139
|
+
"API gateway, data transfer and provisioned-concurrency charges. Serverless carbon counts only the "
|
|
140
|
+
"time requests run and leaves out platform overhead such as warm instances, so it is a lower bound. "
|
|
141
|
+
"The server side assumes on-demand pricing; reserved capacity and savings plans change the cost. "
|
|
142
|
+
"Cold starts, request time limits and concurrency limits can rule serverless out regardless of price. "
|
|
143
|
+
"Power follows Cloud Carbon Footprint's min/max watts per vCPU."),
|
|
144
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Where to check a number: the primary source behind each figure GreenPick reports.
|
|
2
|
+
|
|
3
|
+
GreenPick's estimates are directional, and its results say so. "Go and verify" is only useful if
|
|
4
|
+
it says WHERE, so every result that contains a carbon figure or a dollar figure also gets a short
|
|
5
|
+
`verify_at` list: which kind of figure, what it is based on, and the public page to check it against.
|
|
6
|
+
|
|
7
|
+
Kept deliberately small and stable: top-level pages that publish the data (grid operators, the
|
|
8
|
+
cloud providers' own pricing pages), not deep links that move. Additive only: it never changes or
|
|
9
|
+
removes an existing field.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Iterator
|
|
14
|
+
|
|
15
|
+
# Carbon: the base source name (after stripping "typical_") -> (what it is, where to check it).
|
|
16
|
+
_CARBON = {
|
|
17
|
+
"eia": ("US grid data from EIA-930 (hourly generation by fuel)",
|
|
18
|
+
"https://www.eia.gov/electricity/gridmonitor/"),
|
|
19
|
+
"entsoe": ("EU grid data from the ENTSO-E Transparency Platform (generation by fuel)",
|
|
20
|
+
"https://transparency.entsoe.eu/"),
|
|
21
|
+
"hq": ("Québec grid data from Hydro-Québec open data", "https://donnees.hydroquebec.com/"),
|
|
22
|
+
"ieso": ("Ontario grid data from IESO", "https://www.ieso.ca/en/Power-Data"),
|
|
23
|
+
"ember_annual": ("Yearly national grid intensity from Ember, via Our World in Data",
|
|
24
|
+
"https://ourworldindata.org/grapher/carbon-intensity-electricity"),
|
|
25
|
+
"electricitymaps": ("Grid data from Electricity Maps", "https://www.electricitymaps.com/"),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Cost: provider -> (compute price page, data-transfer price page).
|
|
29
|
+
_PRICING = {
|
|
30
|
+
"aws": ("https://aws.amazon.com/ec2/pricing/on-demand/",
|
|
31
|
+
"https://aws.amazon.com/ec2/pricing/on-demand/"),
|
|
32
|
+
"gcp": ("https://cloud.google.com/compute/vm-instance-pricing",
|
|
33
|
+
"https://cloud.google.com/vpc/network-pricing"),
|
|
34
|
+
"azure": ("https://azure.microsoft.com/en-us/pricing/details/virtual-machines/linux/",
|
|
35
|
+
"https://azure.microsoft.com/en-us/pricing/details/bandwidth/"),
|
|
36
|
+
}
|
|
37
|
+
_PROVIDER_NAME = {"aws": "AWS", "gcp": "Google Cloud", "azure": "Azure"}
|
|
38
|
+
_SERVERLESS = {"aws": ("AWS Lambda", "https://aws.amazon.com/lambda/pricing/"),
|
|
39
|
+
"gcp": ("Cloud Run", "https://cloud.google.com/run/pricing"),
|
|
40
|
+
"azure": ("Azure Functions", "https://azure.microsoft.com/en-us/pricing/details/functions/")}
|
|
41
|
+
|
|
42
|
+
_SOURCE_KEYS = ("source", "carbon_source", "transfer_carbon_source",
|
|
43
|
+
"from_carbon_source", "to_carbon_source")
|
|
44
|
+
_PROVIDER_KEYS = ("provider", "compute_provider", "data_provider", "from_provider", "to_provider")
|
|
45
|
+
_LOCATION_KEYS = ("recommended_region", "compute_location", "data_location", "from", "to")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _walk(obj: Any) -> Iterator[tuple[str, Any]]:
|
|
49
|
+
if isinstance(obj, dict):
|
|
50
|
+
for k, v in obj.items():
|
|
51
|
+
yield k, v
|
|
52
|
+
yield from _walk(v)
|
|
53
|
+
elif isinstance(obj, list):
|
|
54
|
+
for v in obj:
|
|
55
|
+
yield from _walk(v)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def verify_at(result: dict) -> list[dict]:
|
|
59
|
+
"""The primary sources behind the carbon and cost figures in a tool result (may be empty)."""
|
|
60
|
+
carbon: set[str] = set()
|
|
61
|
+
providers: set[str] = set()
|
|
62
|
+
has_cost = has_egress = has_serverless = False
|
|
63
|
+
for key, value in _walk(result):
|
|
64
|
+
if key in _SOURCE_KEYS and isinstance(value, str):
|
|
65
|
+
base = value[len("typical_"):] if value.startswith("typical_") else value
|
|
66
|
+
if base in _CARBON:
|
|
67
|
+
carbon.add(base)
|
|
68
|
+
elif key in _PROVIDER_KEYS and value in _PRICING:
|
|
69
|
+
providers.add(value)
|
|
70
|
+
elif key in _LOCATION_KEYS and isinstance(value, str) and "/" in value:
|
|
71
|
+
prov = value.split("/", 1)[0]
|
|
72
|
+
if prov in _PRICING:
|
|
73
|
+
providers.add(prov)
|
|
74
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
75
|
+
if "usd" not in key:
|
|
76
|
+
continue
|
|
77
|
+
if "serverless" in key:
|
|
78
|
+
has_serverless = True
|
|
79
|
+
elif "egress" in key:
|
|
80
|
+
has_egress = True
|
|
81
|
+
elif "cost" in key or "saving" in key: # compute cost or a saving on it
|
|
82
|
+
has_cost = True
|
|
83
|
+
|
|
84
|
+
out: list[dict] = []
|
|
85
|
+
for base in sorted(carbon):
|
|
86
|
+
what, url = _CARBON[base]
|
|
87
|
+
out.append({"figure": "grid carbon intensity", "based_on": what, "url": url})
|
|
88
|
+
for prov in sorted(providers):
|
|
89
|
+
compute_url, transfer_url = _PRICING[prov]
|
|
90
|
+
name = _PROVIDER_NAME[prov]
|
|
91
|
+
if has_cost:
|
|
92
|
+
out.append({"figure": "compute cost", "based_on": f"{name} on-demand list prices (dated)",
|
|
93
|
+
"url": compute_url})
|
|
94
|
+
if has_serverless:
|
|
95
|
+
product, url = _SERVERLESS[prov]
|
|
96
|
+
out.append({"figure": "serverless cost", "based_on": f"{product} list prices (dated)", "url": url})
|
|
97
|
+
if has_egress:
|
|
98
|
+
out.append({"figure": "data-transfer (egress) cost",
|
|
99
|
+
"based_on": f"{name} data-transfer prices (dated)", "url": transfer_url})
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def with_sources(result: dict) -> dict:
|
|
104
|
+
"""Add `verify_at` to a result that contains carbon or cost figures; leave others unchanged."""
|
|
105
|
+
if isinstance(result, dict) and "error" not in result:
|
|
106
|
+
links = verify_at(result)
|
|
107
|
+
if links:
|
|
108
|
+
result["verify_at"] = links
|
|
109
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: greenpick
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Design-time sustainability co-pilot (MCP server): greener, cheaper cloud infrastructure decisions with carbon + cost + latency tradeoffs.
|
|
5
5
|
Author-email: "Data Science Advisers, LLC" <info@datascienceadvisers.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -44,7 +44,7 @@ Dynamic: license-file
|
|
|
44
44
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License: MIT" />
|
|
45
45
|
<img src="https://img.shields.io/badge/tests-passing-brightgreen" alt="Tests: passing" />
|
|
46
46
|
<img src="https://img.shields.io/badge/python-3.11%2B-blue" alt="Python 3.11+" />
|
|
47
|
-
<img src="https://img.shields.io/badge/MCP%20tools-
|
|
47
|
+
<img src="https://img.shields.io/badge/MCP%20tools-20-0ea5e9" alt="MCP tools: 20" />
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
50
|
A **design-time sustainability co-pilot** that runs as an MCP server inside your vibe-coding
|
|
@@ -52,7 +52,7 @@ environment (Cursor / Claude Code / Kiro). Ask it where to deploy, when to run a
|
|
|
52
52
|
right-size — and it answers with an honest **carbon + cost + latency** tradeoff, in-editor, in
|
|
53
53
|
seconds. (It responds when you ask; auto-nudging on infra-code changes is on the roadmap.)
|
|
54
54
|
|
|
55
|
-
> **Status: working —
|
|
55
|
+
> **Status: working — 20 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
|
|
56
56
|
> IESO — no key needed), cross-cloud cost
|
|
57
57
|
> parity (Vantage-refreshed), per-axis R/Y/G ratings, carbon-aware scheduling + day-ahead forecast,
|
|
58
58
|
> GPU-training **and LLM-inference** carbon, right-sizing, serving-pattern and data-locality/residency
|
|
@@ -143,7 +143,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
143
143
|
|
|
144
144
|
## MCP tools
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
20 tools, grouped by what you're trying to do.
|
|
147
147
|
|
|
148
148
|
### Pick a region
|
|
149
149
|
|
|
@@ -176,6 +176,7 @@ See `docs/user-questions-backlog.md`.
|
|
|
176
176
|
|------|--------------|
|
|
177
177
|
| `greener_instance_family` | A greener same-size family in the same region via perf-per-watt (newer generation / ARM Graviton-class does the same work on less energy). |
|
|
178
178
|
| `rightsizing_check` | Right-sizing (the #1 FinOps win) with a carbon twin: if you're over-provisioned, a smaller size cuts **both** cost and carbon → the right-sized target with monthly $ and kg CO₂ saving. |
|
|
179
|
+
| `serverless_check` | For a mostly idle or spiky service: an always-on server vs. paying per request (AWS Lambda, Cloud Run). Monthly cost of each, the break-even request count, and estimated carbon (serverless as a lower bound). |
|
|
179
180
|
| `offhours_check` | For dev/test/staging that doesn't need to run 24/7: the monthly $ and kg CO2 saved by running it only N hours a week. Compute only, directional. |
|
|
180
181
|
| `data_locality_egress_check` | Warn when running compute away from where your data lives backfires: directional egress $ + coarse transfer carbon, and whether a greener-region move is still a net win. |
|
|
181
182
|
| `migration_payoff` | Does moving the **data** to the compute's region pay off? Payback in months, one-off vs monthly egress and transfer carbon, net over 12 months. Egress only. |
|
|
@@ -226,6 +227,8 @@ flow-traced consumption. Cost is a ballpark, not a quote. Latency is a geographi
|
|
|
226
227
|
|
|
227
228
|
## Quick start
|
|
228
229
|
|
|
230
|
+
<a href="https://mcprush.com/bithug01/greenpick-mcp"><img src="https://mcprush.com/badge/greenpick-mcp/card.svg" width="220" alt="GreenPick MCP server on mcprush" /></a>
|
|
231
|
+
|
|
229
232
|
```bash
|
|
230
233
|
# Python 3.11+
|
|
231
234
|
pip install greenpick # from PyPI
|
|
@@ -50,7 +50,9 @@ src/greenpick/services/recommender.py
|
|
|
50
50
|
src/greenpick/services/residency_service.py
|
|
51
51
|
src/greenpick/services/rightsizing_service.py
|
|
52
52
|
src/greenpick/services/scheduling_service.py
|
|
53
|
+
src/greenpick/services/serverless_service.py
|
|
53
54
|
src/greenpick/services/serving_service.py
|
|
55
|
+
src/greenpick/services/sources.py
|
|
54
56
|
src/greenpick/services/training_service.py
|
|
55
57
|
src/greenpick/services/typical_ranking_service.py
|
|
56
58
|
tests/test_services.py
|
|
@@ -1284,7 +1284,7 @@ def test_server_sends_instructions_that_lead_with_rightsizing():
|
|
|
1284
1284
|
assert "mock" in text and "TOTAL" in text and "data_locality_egress_check" in text
|
|
1285
1285
|
# Every tool named in the instructions actually exists.
|
|
1286
1286
|
import re, asyncio
|
|
1287
|
-
named = set(re.findall(r"\b[a-z]+(?:_[a-z]+)+\b", text)) - {"wall_clock_hours", "gpu_hours", "residency_note", "live_data_hint"}
|
|
1287
|
+
named = set(re.findall(r"\b[a-z]+(?:_[a-z]+)+\b", text)) - {"wall_clock_hours", "gpu_hours", "residency_note", "live_data_hint", "verify_at"}
|
|
1288
1288
|
registered = {t.name for t in asyncio.run(server.mcp.list_tools())}
|
|
1289
1289
|
assert named <= registered, named - registered
|
|
1290
1290
|
|
|
@@ -1802,7 +1802,7 @@ def test_every_tool_declares_all_four_annotation_hints():
|
|
|
1802
1802
|
import asyncio
|
|
1803
1803
|
from greenpick import server
|
|
1804
1804
|
tools = asyncio.run(server.mcp.list_tools())
|
|
1805
|
-
assert len(tools) ==
|
|
1805
|
+
assert len(tools) == 20
|
|
1806
1806
|
for t in tools:
|
|
1807
1807
|
a = t.annotations
|
|
1808
1808
|
assert a is not None, t.name
|
|
@@ -2033,3 +2033,126 @@ async def test_rightsizing_fraction_still_works_and_ram_is_checked(monkeypatch):
|
|
|
2033
2033
|
async def test_training_rejects_percentage_utilization():
|
|
2034
2034
|
r = await _tool("estimate_training_carbon")(gpu="a100", gpu_count=8, gpu_hours=40, utilization=60)
|
|
2035
2035
|
assert "error" in r and "Did you mean 0.6?" in r["error"]
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
# --- 0.6.3: verify_at says where to check each carbon and cost figure (additive only) ---
|
|
2039
|
+
def test_verify_at_maps_sources_and_costs():
|
|
2040
|
+
from greenpick.services.sources import verify_at
|
|
2041
|
+
r = {"carbon_source": "typical_eia", "provider": "aws", "cost_now_usd_per_month": 10.0}
|
|
2042
|
+
figs = {(v["figure"], v["url"]) for v in verify_at(r)}
|
|
2043
|
+
assert ("grid carbon intensity", "https://www.eia.gov/electricity/gridmonitor/") in figs
|
|
2044
|
+
assert ("compute cost", "https://aws.amazon.com/ec2/pricing/on-demand/") in figs
|
|
2045
|
+
eg = verify_at({"data_location": "gcp/us-east4", "est_monthly_egress_usd_low": 1.0, "transfer_carbon_source": "ember_annual"})
|
|
2046
|
+
assert {v["figure"] for v in eg} == {"data-transfer (egress) cost", "grid carbon intensity"}
|
|
2047
|
+
assert verify_at({"provider": "aws", "net_over_horizon_usd_low": 3.0}) == [] # not a compute cost
|
|
2048
|
+
assert verify_at({"note": "nothing to check"}) == []
|
|
2049
|
+
|
|
2050
|
+
|
|
2051
|
+
@pytest.mark.asyncio
|
|
2052
|
+
async def test_tools_add_verify_at_but_not_on_errors(monkeypatch):
|
|
2053
|
+
from greenpick import server
|
|
2054
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
2055
|
+
r = await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32, cpu_utilization=0.15)
|
|
2056
|
+
assert {v["figure"] for v in r["verify_at"]} == {"grid carbon intensity", "compute cost"}
|
|
2057
|
+
rank = _tool("greenest_regions_on_average")(month=7, regions=["us-west-2", "ap-northeast-1"])
|
|
2058
|
+
assert any("ourworldindata" in v["url"] for v in rank["verify_at"])
|
|
2059
|
+
bad = await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32, cpu_utilization=15)
|
|
2060
|
+
assert "error" in bad and "verify_at" not in bad
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
# --- The notebook starter prompt (docs/using-from-a-notebook.md) relies on these fields. ---
|
|
2064
|
+
# Testers and workshop attendees run that prompt against whatever version pip installs, so a
|
|
2065
|
+
# release must never rename or drop them.
|
|
2066
|
+
@pytest.mark.asyncio
|
|
2067
|
+
async def test_fields_the_notebook_prompt_relies_on(monkeypatch):
|
|
2068
|
+
from greenpick import server
|
|
2069
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
2070
|
+
now = await _tool("region_carbon_now")(provider="aws", region="us-east-1")
|
|
2071
|
+
assert {"source", "quality", "live_data_hint"} <= set(now)
|
|
2072
|
+
eg = await _tool("data_locality_egress_check")(compute_provider="aws", compute_region="eu-north-1",
|
|
2073
|
+
data_provider="aws", data_region="us-east-1",
|
|
2074
|
+
monthly_cross_boundary_gb=100)
|
|
2075
|
+
assert next(iter(eg)) == "summary" and eg["summary"].startswith("NO NET VERDICT")
|
|
2076
|
+
assert {"residency_note", "limitations"} <= set(eg)
|
|
2077
|
+
mp = await _tool("migration_payoff")("aws", "us-east-1", "aws", "eu-north-1", 2000, 300)
|
|
2078
|
+
assert next(iter(mp)) == "summary" and "net_over_horizon_usd_low" in mp and "net_over_horizon_usd_high" in mp
|
|
2079
|
+
pr = await _tool("region_move_justification")("aws", "us-east-1", "aws", "eu-north-1", 4, 16)
|
|
2080
|
+
assert next(iter(pr)) == "summary" and pr["pr_text"]
|
|
2081
|
+
off = await _tool("offhours_check")(provider="aws", region="us-east-1", vcpu=4, ram_gb=16, hours_per_week_on=50)
|
|
2082
|
+
assert {"monthly_usd_saving", "limitations"} <= set(off)
|
|
2083
|
+
assert "error" in await _tool("offhours_check")(provider="aws", region="us-east-1", vcpu=4, ram_gb=16, hours_per_week_on=200)
|
|
2084
|
+
assert "error" in await _tool("rightsizing_check")(provider="aws", region="us-east-1", vcpu=8, ram_gb=32, cpu_utilization=15)
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
@pytest.mark.asyncio
|
|
2088
|
+
async def test_reverse_pr_text_does_not_repeat_backfire():
|
|
2089
|
+
from greenpick.services import justification_service as js
|
|
2090
|
+
r = await js.region_move_justification(MockCarbonRepository(), "aws", "eu-north-1", "aws", "us-east-1",
|
|
2091
|
+
4, 16, monthly_cross_boundary_gb=100)
|
|
2092
|
+
assert "would RAISE" in r["pr_text"] and "backfires" not in r["pr_text"]
|
|
2093
|
+
assert "unless the data moves too" not in r["pr_text"] and "on top." in r["pr_text"]
|
|
2094
|
+
|
|
2095
|
+
|
|
2096
|
+
# --- 0.7.0: utilisation-aware power and serverless_check ---
|
|
2097
|
+
def test_utilisation_power_spans_min_to_max_and_full_load_matches_flat_model():
|
|
2098
|
+
from greenpick.services import carbon_service as cs
|
|
2099
|
+
from greenpick.models import CarbonReading
|
|
2100
|
+
assert cs.watts_per_vcpu_at("aws", 0) == 0.74 and cs.watts_per_vcpu_at("aws", 1) == 3.5
|
|
2101
|
+
assert abs(cs.watts_per_vcpu_at("gcp", 0.5) - (0.71 + 0.5 * (4.26 - 0.71))) < 1e-9
|
|
2102
|
+
wl = Workload(vcpu=4, ram_gb=16, hours_per_month=730)
|
|
2103
|
+
rd = CarbonReading(region="us-east-1", provider="aws", g_co2_per_kwh=400, source="mock",
|
|
2104
|
+
timestamp="2026-11-10T00:00:00Z", quality="mock")
|
|
2105
|
+
assert cs.estimate_kg_co2_per_month_at(wl, rd, 1.0) == cs.estimate_kg_co2_per_month(wl, rd)
|
|
2106
|
+
assert cs.estimate_kg_co2_per_month_at(wl, rd, 0.05) < cs.estimate_kg_co2_per_month(wl, rd) / 2
|
|
2107
|
+
|
|
2108
|
+
|
|
2109
|
+
@pytest.mark.asyncio
|
|
2110
|
+
async def test_estimate_workload_carbon_utilisation_is_optional_and_additive(monkeypatch):
|
|
2111
|
+
from greenpick import server
|
|
2112
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
2113
|
+
plain = await _tool("estimate_workload_carbon")(provider="aws", region="us-east-1", vcpu=4, ram_gb=16)
|
|
2114
|
+
assert "avg_cpu_utilization" not in plain and "estimated_kg_co2_per_month_at_full_load" not in plain
|
|
2115
|
+
low = await _tool("estimate_workload_carbon")(provider="aws", region="us-east-1", vcpu=4, ram_gb=16,
|
|
2116
|
+
avg_cpu_utilization=0.05)
|
|
2117
|
+
assert low["estimated_kg_co2_per_month_at_full_load"] == plain["estimated_kg_co2_per_month"]
|
|
2118
|
+
assert low["estimated_kg_co2_per_month"] < plain["estimated_kg_co2_per_month"]
|
|
2119
|
+
bad = await _tool("estimate_workload_carbon")(provider="aws", region="us-east-1", vcpu=4, ram_gb=16,
|
|
2120
|
+
avg_cpu_utilization=5)
|
|
2121
|
+
assert "Did you mean 0.05?" in bad["error"]
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
@pytest.mark.asyncio
|
|
2125
|
+
async def test_serverless_check_quiet_vs_busy_and_breakeven(monkeypatch):
|
|
2126
|
+
from greenpick import server
|
|
2127
|
+
monkeypatch.setattr(server, "_repo", TypicalCarbonRepository())
|
|
2128
|
+
t = _tool("serverless_check")
|
|
2129
|
+
quiet = await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.05,
|
|
2130
|
+
monthly_requests=1_000_000, avg_duration_ms=100, function_memory_gb=0.5)
|
|
2131
|
+
assert next(iter(quiet)) == "summary" and quiet["verdict"] == "serverless_cheaper"
|
|
2132
|
+
assert quiet["within_free_tier"] is True and "free tier" in quiet["summary"]
|
|
2133
|
+
busy = await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.6,
|
|
2134
|
+
monthly_requests=100_000_000, avg_duration_ms=100, function_memory_gb=0.5)
|
|
2135
|
+
assert busy["verdict"] == "server_cheaper" and busy["monthly_usd_saving"] < 0
|
|
2136
|
+
# The break-even request count is where serverless cost meets the server's.
|
|
2137
|
+
be = quiet["breakeven_monthly_requests"]
|
|
2138
|
+
at_be = await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.05,
|
|
2139
|
+
monthly_requests=be, avg_duration_ms=100, function_memory_gb=0.5)
|
|
2140
|
+
assert abs(at_be["serverless_cost_usd_per_month"] - at_be["server_cost_usd_per_month"]) < 0.05
|
|
2141
|
+
assert {v["figure"] for v in quiet["verify_at"]} >= {"compute cost", "serverless cost"}
|
|
2142
|
+
run = await t(provider="gcp", region="us-central1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.05,
|
|
2143
|
+
monthly_requests=1_000_000, avg_duration_ms=100, function_memory_gb=0.5)
|
|
2144
|
+
assert run["serverless_cost_usd_per_month"] is not None
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
@pytest.mark.asyncio
|
|
2148
|
+
async def test_serverless_check_azure_is_carbon_only_and_inputs_validated():
|
|
2149
|
+
t = _tool("serverless_check")
|
|
2150
|
+
az = await t(provider="azure", region="eastus", vcpu=2, ram_gb=4, avg_cpu_utilization=0.05,
|
|
2151
|
+
monthly_requests=1_000_000, avg_duration_ms=100)
|
|
2152
|
+
assert az["verdict"] == "carbon_only" and az["serverless_cost_usd_per_month"] is None
|
|
2153
|
+
assert "error" in await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=5,
|
|
2154
|
+
monthly_requests=1000, avg_duration_ms=100)
|
|
2155
|
+
assert "error" in await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.1,
|
|
2156
|
+
monthly_requests=1000, avg_duration_ms=0)
|
|
2157
|
+
assert "error" in await t(provider="aws", region="us-east-1", vcpu=2, ram_gb=4, avg_cpu_utilization=0.1,
|
|
2158
|
+
monthly_requests=1000, avg_duration_ms=100, function_memory_gb=64)
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "Simple, documented power model for the carbon ESTIMATE. Turns a workload (vCPU, RAM) into average watts, then kWh/month. These are deliberately rough published-style coefficients; this is an estimate, not a measurement. PUE is the datacenter overhead multiplier.",
|
|
3
|
-
"watts_per_vcpu": 3.5,
|
|
4
|
-
"watts_per_gb_ram": 0.4,
|
|
5
|
-
"pue_default": 1.2,
|
|
6
|
-
"_sources": "Coefficients are order-of-magnitude figures consistent with public cloud power-model literature (e.g. Cloud Carbon Footprint methodology). Replace with cited values before any non-directional use."
|
|
7
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "DATED, STATIC pricing ballpark for v1. Deterministic \u2014 the LLM must never invent these. Figures are rough on-demand approximations for a general-purpose vCPU+RAM service and per-GB internet egress; refresh periodically and update _as_of. NOT a quote.",
|
|
3
|
-
"_as_of": "2026-10-12",
|
|
4
|
-
"_units": {
|
|
5
|
-
"compute": "USD per vCPU-hour and USD per GB-RAM-hour",
|
|
6
|
-
"egress": "USD per GB to internet"
|
|
7
|
-
},
|
|
8
|
-
"_egress_comment": "Egress is NOT one price — it's a tier ladder by WHERE the bytes cross. We price the locality tier (same-region / cross-region / cross-cloud+internet) as a LOW-HIGH RANGE because region-pair rates vary. Per-GB, USD, DIRECTIONAL list prices as of _as_of; excludes free-tier allowances, committed-use/negotiated discounts, and NAT/load-balancer processing fees. Not a quote. Keys: internet rate stays per-provider (differs most); same-region & cross-region tiers are broadly consistent across AWS/GCP/Azure so we use one shared range.",
|
|
9
|
-
"_egress_tiers_usd_per_gb": {
|
|
10
|
-
"same_region": { "low": 0.0, "high": 0.01, "note": "Same region: free within an AZ over private IP; ~$0.01/GB each way if it crosses AZs." },
|
|
11
|
-
"cross_region_same_cloud": { "low": 0.01, "high": 0.02, "note": "Inter-region within one cloud: most pairs ~$0.01-0.02/GB; a few source regions/continent pairs run higher." },
|
|
12
|
-
"cross_cloud": { "low": 0.085, "high": 0.12, "note": "Leaving the cloud to another provider or the public internet: the expensive tier; GCP Premium ~$0.12, AWS ~$0.09, Azure ~$0.087 at entry bands." }
|
|
13
|
-
},
|
|
14
|
-
"_egress_sources": "AWS/GCP/Azure published data-transfer pricing pages + corroborating 2026 guides (usage.ai, nops.io, egresscost.com, redresscompliance, bacancy, tatacommunications). Rounded to ranges; verify against the provider pricing page for a specific region pair.",
|
|
15
|
-
"aws": {
|
|
16
|
-
"compute_usd_per_vcpu_hour": 0.0469,
|
|
17
|
-
"compute_usd_per_gb_ram_hour": 0.0039,
|
|
18
|
-
"egress_usd_per_gb": 0.09
|
|
19
|
-
},
|
|
20
|
-
"gcp": {
|
|
21
|
-
"compute_usd_per_vcpu_hour": 0.0438,
|
|
22
|
-
"compute_usd_per_gb_ram_hour": 0.0037,
|
|
23
|
-
"egress_usd_per_gb": 0.085
|
|
24
|
-
},
|
|
25
|
-
"azure": {
|
|
26
|
-
"compute_usd_per_vcpu_hour": 0.0472,
|
|
27
|
-
"compute_usd_per_gb_ram_hour": 0.0039,
|
|
28
|
-
"egress_usd_per_gb": 0.087
|
|
29
|
-
},
|
|
30
|
-
"_source": "Compute coefficients for [aws+azure+gcp] derived from Vantage live data using ONE matched method: general-purpose 4GB/vCPU families (AWS m5/m6i, GCP n2-standard, Azure D-series v5), on-demand Linux, mean $/vCPU across regions, 25% split to RAM. Egress remains static. Apples-to-apples DIRECTIONAL comparison, not a quote."
|
|
31
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
"""Carbon readings (via the repository) + a workload CO2 ESTIMATE.
|
|
2
|
-
|
|
3
|
-
The estimate turns a workload into average watts (power model) -> kWh/month ->
|
|
4
|
-
multiply by grid intensity. Clearly an estimate, not a measurement.
|
|
5
|
-
"""
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from ..models import CarbonReading, Workload, Provider
|
|
9
|
-
from ..data.repository import CarbonRepository
|
|
10
|
-
from ..data.static_data import power_model
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
async def carbon_now(repo: CarbonRepository, provider: Provider, region: str) -> CarbonReading:
|
|
14
|
-
return await repo.carbon_now(provider, region)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def workload_kwh_per_month(workload: Workload) -> float:
|
|
18
|
-
pm = power_model()
|
|
19
|
-
watts = workload.vcpu * pm["watts_per_vcpu"] + workload.ram_gb * pm["watts_per_gb_ram"]
|
|
20
|
-
watts *= pm["pue_default"]
|
|
21
|
-
kwh = watts / 1000.0 * workload.hours_per_month
|
|
22
|
-
return kwh
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def estimate_kg_co2_per_month(workload: Workload, reading: CarbonReading) -> float:
|
|
26
|
-
kwh = workload_kwh_per_month(workload)
|
|
27
|
-
grams = kwh * reading.g_co2_per_kwh
|
|
28
|
-
return round(grams / 1000.0, 2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|