greenpick 0.6.3__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.3/src/greenpick.egg-info → greenpick-0.7.0}/PKG-INFO +5 -4
- {greenpick-0.6.3 → greenpick-0.7.0}/README.md +4 -3
- {greenpick-0.6.3 → greenpick-0.7.0}/pyproject.toml +1 -1
- {greenpick-0.6.3 → 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.3 → greenpick-0.7.0}/src/greenpick/server.py +52 -6
- greenpick-0.7.0/src/greenpick/services/carbon_service.py +51 -0
- greenpick-0.7.0/src/greenpick/services/serverless_service.py +144 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/sources.py +10 -2
- {greenpick-0.6.3 → greenpick-0.7.0/src/greenpick.egg-info}/PKG-INFO +5 -4
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/SOURCES.txt +1 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/tests/test_services.py +66 -1
- greenpick-0.6.3/src/greenpick/data/power.json +0 -7
- greenpick-0.6.3/src/greenpick/data/pricing.json +0 -31
- greenpick-0.6.3/src/greenpick/services/carbon_service.py +0 -28
- {greenpick-0.6.3 → greenpick-0.7.0}/LICENSE +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/setup.cfg +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/__main__.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/config.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/__init__.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/_net.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/annual.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/auto_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/canada_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/eia_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/emission_factors.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/entsoe_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/gpu_power.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/inference_energy.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/instance_families.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/mock_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/network.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/regions.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/static_data.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/typical.json +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/typical_repository.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/models.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/__init__.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/cost_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/data_hint.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/forecast_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/inference_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/instance_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/justification_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/latency_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/locality_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/migration_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/offhours_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/recommender.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/residency_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/rightsizing_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/scheduling_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/serving_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/training_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/typical_ranking_service.py +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/entry_points.txt +0 -0
- {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/requires.txt +0 -0
- {greenpick-0.6.3 → 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. |
|
|
@@ -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. |
|
|
@@ -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,7 +26,7 @@ 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
|
|
@@ -41,7 +41,8 @@ GreenPick advises on cloud infrastructure decisions BEFORE you deploy, weighing
|
|
|
41
41
|
latency together. Strongest uses, in order:
|
|
42
42
|
1. Right-sizing (rightsizing_check): an over-provisioned instance cut to a standard, buyable size \
|
|
43
43
|
saves money and carbon in the same move. Lead with the monthly dollar saving. For dev, test or other \
|
|
44
|
-
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.
|
|
45
46
|
2. Scheduling deferrable jobs: cleanest_hours_to_run (one-off), cleanest_time_of_day (recurring), \
|
|
46
47
|
carbon_forecast (EU day-ahead).
|
|
47
48
|
3. GPU training and LLM inference footprints: estimate_training_carbon (gpu_hours is the TOTAL across \
|
|
@@ -148,13 +149,19 @@ async def estimate_workload_carbon(
|
|
|
148
149
|
vcpu: float,
|
|
149
150
|
ram_gb: float,
|
|
150
151
|
hours_per_month: float = 730,
|
|
152
|
+
avg_cpu_utilization: Optional[float] = None,
|
|
151
153
|
) -> dict:
|
|
152
154
|
"""Rough kgCO2/month for a workload running in a region. An ESTIMATE (documented power
|
|
153
|
-
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}
|
|
154
161
|
reading = await carbon_service.carbon_now(_repo, provider, region) # type: ignore[arg-type]
|
|
155
162
|
wl = Workload(vcpu=vcpu, ram_gb=ram_gb, hours_per_month=hours_per_month)
|
|
156
163
|
kg = carbon_service.estimate_kg_co2_per_month(wl, reading)
|
|
157
|
-
|
|
164
|
+
out = {
|
|
158
165
|
"provider": provider,
|
|
159
166
|
"region": region,
|
|
160
167
|
"estimated_kg_co2_per_month": kg,
|
|
@@ -162,7 +169,15 @@ async def estimate_workload_carbon(
|
|
|
162
169
|
"carbon_source": reading.source,
|
|
163
170
|
"carbon_quality": reading.quality,
|
|
164
171
|
"note": "Estimate only: documented power model x grid intensity. Not a measurement.",
|
|
165
|
-
}
|
|
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)
|
|
166
181
|
|
|
167
182
|
|
|
168
183
|
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
@@ -273,6 +288,37 @@ async def rightsizing_check(
|
|
|
273
288
|
return with_hint(advice.model_dump())
|
|
274
289
|
|
|
275
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
|
+
|
|
276
322
|
@mcp.tool(annotations=LIVE_DATA_TOOL)
|
|
277
323
|
async def offhours_check(
|
|
278
324
|
provider: str,
|
|
@@ -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)
|
|
@@ -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
|
+
}
|
|
@@ -35,6 +35,9 @@ _PRICING = {
|
|
|
35
35
|
"https://azure.microsoft.com/en-us/pricing/details/bandwidth/"),
|
|
36
36
|
}
|
|
37
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/")}
|
|
38
41
|
|
|
39
42
|
_SOURCE_KEYS = ("source", "carbon_source", "transfer_carbon_source",
|
|
40
43
|
"from_carbon_source", "to_carbon_source")
|
|
@@ -56,7 +59,7 @@ def verify_at(result: dict) -> list[dict]:
|
|
|
56
59
|
"""The primary sources behind the carbon and cost figures in a tool result (may be empty)."""
|
|
57
60
|
carbon: set[str] = set()
|
|
58
61
|
providers: set[str] = set()
|
|
59
|
-
has_cost = has_egress = False
|
|
62
|
+
has_cost = has_egress = has_serverless = False
|
|
60
63
|
for key, value in _walk(result):
|
|
61
64
|
if key in _SOURCE_KEYS and isinstance(value, str):
|
|
62
65
|
base = value[len("typical_"):] if value.startswith("typical_") else value
|
|
@@ -71,7 +74,9 @@ def verify_at(result: dict) -> list[dict]:
|
|
|
71
74
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
72
75
|
if "usd" not in key:
|
|
73
76
|
continue
|
|
74
|
-
if "
|
|
77
|
+
if "serverless" in key:
|
|
78
|
+
has_serverless = True
|
|
79
|
+
elif "egress" in key:
|
|
75
80
|
has_egress = True
|
|
76
81
|
elif "cost" in key or "saving" in key: # compute cost or a saving on it
|
|
77
82
|
has_cost = True
|
|
@@ -86,6 +91,9 @@ def verify_at(result: dict) -> list[dict]:
|
|
|
86
91
|
if has_cost:
|
|
87
92
|
out.append({"figure": "compute cost", "based_on": f"{name} on-demand list prices (dated)",
|
|
88
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})
|
|
89
97
|
if has_egress:
|
|
90
98
|
out.append({"figure": "data-transfer (egress) cost",
|
|
91
99
|
"based_on": f"{name} data-transfer prices (dated)", "url": transfer_url})
|
|
@@ -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. |
|
|
@@ -50,6 +50,7 @@ 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
|
|
54
55
|
src/greenpick/services/sources.py
|
|
55
56
|
src/greenpick/services/training_service.py
|
|
@@ -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
|
|
@@ -2091,3 +2091,68 @@ async def test_reverse_pr_text_does_not_repeat_backfire():
|
|
|
2091
2091
|
4, 16, monthly_cross_boundary_gb=100)
|
|
2092
2092
|
assert "would RAISE" in r["pr_text"] and "backfires" not in r["pr_text"]
|
|
2093
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
|
|
File without changes
|
|
File without changes
|