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.
Files changed (63) hide show
  1. {greenpick-0.6.3/src/greenpick.egg-info → greenpick-0.7.0}/PKG-INFO +5 -4
  2. {greenpick-0.6.3 → greenpick-0.7.0}/README.md +4 -3
  3. {greenpick-0.6.3 → greenpick-0.7.0}/pyproject.toml +1 -1
  4. {greenpick-0.6.3 → 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.3 → greenpick-0.7.0}/src/greenpick/server.py +52 -6
  8. greenpick-0.7.0/src/greenpick/services/carbon_service.py +51 -0
  9. greenpick-0.7.0/src/greenpick/services/serverless_service.py +144 -0
  10. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/sources.py +10 -2
  11. {greenpick-0.6.3 → greenpick-0.7.0/src/greenpick.egg-info}/PKG-INFO +5 -4
  12. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/SOURCES.txt +1 -0
  13. {greenpick-0.6.3 → greenpick-0.7.0}/tests/test_services.py +66 -1
  14. greenpick-0.6.3/src/greenpick/data/power.json +0 -7
  15. greenpick-0.6.3/src/greenpick/data/pricing.json +0 -31
  16. greenpick-0.6.3/src/greenpick/services/carbon_service.py +0 -28
  17. {greenpick-0.6.3 → greenpick-0.7.0}/LICENSE +0 -0
  18. {greenpick-0.6.3 → greenpick-0.7.0}/setup.cfg +0 -0
  19. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/__main__.py +0 -0
  20. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/config.py +0 -0
  21. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/__init__.py +0 -0
  22. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/_net.py +0 -0
  23. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/annual.json +0 -0
  24. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/auto_repository.py +0 -0
  25. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/canada_repository.py +0 -0
  26. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/eia_repository.py +0 -0
  27. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/electricitymaps_repository.py +0 -0
  28. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/emission_factors.json +0 -0
  29. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/entsoe_repository.py +0 -0
  30. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/gpu_power.json +0 -0
  31. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/inference_energy.json +0 -0
  32. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/instance_families.json +0 -0
  33. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/mock_repository.py +0 -0
  34. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/network.json +0 -0
  35. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/regions.json +0 -0
  36. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/repository.py +0 -0
  37. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/static_data.py +0 -0
  38. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/typical.json +0 -0
  39. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/data/typical_repository.py +0 -0
  40. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/models.py +0 -0
  41. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/__init__.py +0 -0
  42. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/cost_service.py +0 -0
  43. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/data_hint.py +0 -0
  44. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/forecast_service.py +0 -0
  45. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/inference_service.py +0 -0
  46. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/instance_service.py +0 -0
  47. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/justification_service.py +0 -0
  48. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/latency_service.py +0 -0
  49. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/locality_service.py +0 -0
  50. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/metric_scaffold_service.py +0 -0
  51. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/migration_service.py +0 -0
  52. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/offhours_service.py +0 -0
  53. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/recommender.py +0 -0
  54. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/residency_service.py +0 -0
  55. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/rightsizing_service.py +0 -0
  56. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/scheduling_service.py +0 -0
  57. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/serving_service.py +0 -0
  58. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/training_service.py +0 -0
  59. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick/services/typical_ranking_service.py +0 -0
  60. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/dependency_links.txt +0 -0
  61. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/entry_points.txt +0 -0
  62. {greenpick-0.6.3 → greenpick-0.7.0}/src/greenpick.egg-info/requires.txt +0 -0
  63. {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.6.3
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. |
@@ -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. |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "greenpick"
3
- version = "0.6.3"
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.3"
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,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
- return with_hint({
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 "egress" in key:
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.6.3
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. |
@@ -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) == 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
@@ -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