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.
Files changed (63) hide show
  1. {greenpick-0.6.2/src/greenpick.egg-info → greenpick-0.7.0}/PKG-INFO +7 -4
  2. {greenpick-0.6.2 → greenpick-0.7.0}/README.md +6 -3
  3. {greenpick-0.6.2 → greenpick-0.7.0}/pyproject.toml +1 -1
  4. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/__init__.py +1 -1
  5. greenpick-0.7.0/src/greenpick/data/power.json +21 -0
  6. greenpick-0.7.0/src/greenpick/data/pricing.json +65 -0
  7. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/server.py +57 -9
  8. greenpick-0.7.0/src/greenpick/services/carbon_service.py +51 -0
  9. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/data_hint.py +4 -2
  10. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/justification_service.py +5 -1
  11. greenpick-0.7.0/src/greenpick/services/serverless_service.py +144 -0
  12. greenpick-0.7.0/src/greenpick/services/sources.py +109 -0
  13. {greenpick-0.6.2 → greenpick-0.7.0/src/greenpick.egg-info}/PKG-INFO +7 -4
  14. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/SOURCES.txt +2 -0
  15. {greenpick-0.6.2 → greenpick-0.7.0}/tests/test_services.py +125 -2
  16. greenpick-0.6.2/src/greenpick/data/power.json +0 -7
  17. greenpick-0.6.2/src/greenpick/data/pricing.json +0 -31
  18. greenpick-0.6.2/src/greenpick/services/carbon_service.py +0 -28
  19. {greenpick-0.6.2 → greenpick-0.7.0}/LICENSE +0 -0
  20. {greenpick-0.6.2 → greenpick-0.7.0}/setup.cfg +0 -0
  21. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/__main__.py +0 -0
  22. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/config.py +0 -0
  23. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/__init__.py +0 -0
  24. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/_net.py +0 -0
  25. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/annual.json +0 -0
  26. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/auto_repository.py +0 -0
  27. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/canada_repository.py +0 -0
  28. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/eia_repository.py +0 -0
  29. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
  30. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/emission_factors.json +0 -0
  31. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/entsoe_repository.py +0 -0
  32. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/gpu_power.json +0 -0
  33. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/inference_energy.json +0 -0
  34. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/instance_families.json +0 -0
  35. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/mock_repository.py +0 -0
  36. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/network.json +0 -0
  37. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/regions.json +0 -0
  38. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/repository.py +0 -0
  39. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/static_data.py +0 -0
  40. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/typical.json +0 -0
  41. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/data/typical_repository.py +0 -0
  42. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/models.py +0 -0
  43. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/__init__.py +0 -0
  44. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/cost_service.py +0 -0
  45. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/forecast_service.py +0 -0
  46. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/inference_service.py +0 -0
  47. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/instance_service.py +0 -0
  48. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/latency_service.py +0 -0
  49. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/locality_service.py +0 -0
  50. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
  51. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/migration_service.py +0 -0
  52. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/offhours_service.py +0 -0
  53. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/recommender.py +0 -0
  54. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/residency_service.py +0 -0
  55. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/rightsizing_service.py +0 -0
  56. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/scheduling_service.py +0 -0
  57. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/serving_service.py +0 -0
  58. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/training_service.py +0 -0
  59. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick/services/typical_ranking_service.py +0 -0
  60. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
  61. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/entry_points.txt +0 -0
  62. {greenpick-0.6.2 → greenpick-0.7.0}/src/greenpick.egg-info/requires.txt +0 -0
  63. {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.6.2
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-19-0ea5e9" alt="MCP tools: 19" />
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 — 19 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
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
- 19 tools, grouped by what you're trying to do.
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-19-0ea5e9" alt="MCP tools: 19" />
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 — 19 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
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
- 19 tools, grouped by what you're trying to do.
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.6.2"
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.6.2"
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 19 tools over FastMCP.
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. If a call fails or output is missing, say so rather than describing \
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
- return with_hint({
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; leave others unchanged."""
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 net_kg > 0:
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.6.2
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-19-0ea5e9" alt="MCP tools: 19" />
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 — 19 tools.** Live carbon for US (EIA-930), EU (ENTSO-E) and Canada (Hydro-Québec,
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
- 19 tools, grouped by what you're trying to do.
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) == 19
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