Metadata-Version: 2.4
Name: rakam-frugal-ai
Version: 2026.7.9.1
Summary: Carbon intensity data registry for LLM and embedding models with live Electricity Maps updates
Project-URL: Homepage, https://github.com/Rakam-AI/rakam-frugal-ai
Project-URL: Repository, https://github.com/Rakam-AI/rakam-frugal-ai
Project-URL: Issues, https://github.com/Rakam-AI/rakam-frugal-ai/issues
Author-email: Samuel Gerardin <samuel@rakam.ai>
License: MIT
License-File: LICENSE
Keywords: carbon,co2,electricity-maps,emissions,llm,sustainability
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic-settings>=2.2.1
Requires-Dist: pydantic>=2.7.0
Requires-Dist: python-dotenv>=1.0.1
Provides-Extra: dev
Requires-Dist: anyio>=4.0.0; extra == 'dev'
Requires-Dist: black>=24.3.0; extra == 'dev'
Requires-Dist: build>=1.0.0; extra == 'dev'
Requires-Dist: mypy>=1.9.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.26.0; extra == 'dev'
Requires-Dist: pytest-cov>=6.2.1; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.21.0; extra == 'dev'
Requires-Dist: ruff>=0.4.5; extra == 'dev'
Provides-Extra: tiktoken
Requires-Dist: tiktoken>=0.7.0; extra == 'tiktoken'
Description-Content-Type: text/markdown

# Rakam-frugal-ai

Carbon intensity and per-request CO₂ estimates for LLM and embedding models.
Ships a precomputed, weighted-by-region probability table and parameter-size
estimates for popular models (GPT-4o, GPT-4o-mini, Claude, embedding models,
…). Carbon intensity values are refreshed weekly from
[Electricity Maps](https://www.electricitymaps.com/); no API key is needed at
call time.

## Install

```bash
pip install rakam-frugal-ai
# Optional, for accurate token counting on text fallbacks:
pip install "rakam-frugal-ai[tiktoken]"
```

## Carbon intensity registry (the original API)

```python
import rakam_frugal_ai as ci

m = ci.model("gpt-4o")
m.CI              # CarbonIntensityValue(260.735 gCO2eq/kWh)
float(m.CI)       # 260.735 — raw gCO2eq/kWh, weighted across the model's regions
m.size            # SizeValue(200.00B params)
float(m.size)     # 200.0 — raw billion params
m.provider        # 'openai'
m.type            # <ModelType.LLM: 'llm'>
```

CI values are weighted across each model's known datacenter regions (the
weights are rakam's estimate of how traffic distributes). The number you see
is `Σ P(zone) × CI(zone)` over those regions.

## CO₂ per request

Three entry points, all zero-network at call time:

```python
ci.get_co2_for_model(model_name, region=None)             # → Co2Result, grams/token
ci.get_co2_for_query(query=None, *, model_name,
                     input_tokens=None, output_tokens=None,
                     region=None)                          # → Co2Result, grams for the call
ci.get_co2_for_pipeline(steps, region=None)               # → Co2Result, summed grams
```

`Co2Result` is a `float` subclass — `float(r)` gives the central (mean)
value, `r.detail` carries provenance. Arithmetic on a `Co2Result` returns a
plain `float`; `.detail` is meaningful only on the original lib-returned
value.

### Token rules — applied automatically by `m.type`

You don't have to reason about which tokens count:

| Model type | What counts toward energy | Notes |
|---|---|---|
| **LLM (generation)** | `input_tokens + output_tokens` | For RAG, `input_tokens` MUST be the **full assembled prompt** (system + retrieved chunks + history + query), not just the user query — the library can't infer this; pass `response.usage.prompt_tokens`. |
| **EMBEDDING** | `input_tokens` only | The output is a vector, not generated tokens. If you pass a non-zero `output_tokens` for an embedding model it is ignored with a warning note. |

### Region routing

| `region=` | What it does | `ci_model_confidence.source` | `ci_model_confidence.level` |
|---|---|---|---|
| `None` (default) | Use the model's region-weighted CI from the bundled registry | `"rakam_default_weighted"` | graded — `"high"` / `"medium"` / `"low"` from the model's `location_confidence` |
| Any EM zone code, e.g. `"FR"` | Look up that zone's CI directly in the bundled `zone_ci.json` (zero network) | `"electricitymaps:FR"` | `"measured"` (it's a real reading, not a judgment) |
| Unknown zone | `KeyError` listing available zones | — | — |

The asymmetry is deliberate: a direct zone reading is *measured*; a weighted
blend is a *judgment* graded on its quality.

### Example — single call

```python
r = ci.get_co2_for_query(
    query=None,                 # using real counts from response.usage
    model_name="gpt-4o",
    input_tokens=3000,          # full assembled RAG prompt
    output_tokens=800,
    region="FR",
)

float(r)
# 0.011 g CO2eq for the call

r.detail
# {
#   "co2_grams_mean": 0.0111...,
#   "co2_grams_min":  0.0111...,
#   "co2_grams_max":  0.0111...,   # degenerate range — EcoLogits methodology
#                                  # produces a deterministic point; range is
#                                  # the source's intrinsic range as-is.
#   "model_name": "gpt-4o",
#   "model_type": "llm",
#   "ci_gco2_per_kwh": 19.0,
#   "ci_model_confidence": {"level": "measured", "source": "electricitymaps:FR"},
#   "energy_per_token_kwh": 1.54e-07,
#   "energy_source": "ecologits_estimate",
#   "energy_source_confidence": "estimated",
#   "modelsize_confidence": {
#     "level":               "medium",
#     "source":              "Microsoft/Univ. Washington MEDEC paper (Dec 2024) ...",
#     "used_in_calculation": True,    # EcoLogits fallback → size fed the number
#   },
#   "input_tokens": 3000,
#   "output_tokens": 800,
#   "energy_tokens": 3800,
#   "notes": (
#     "energy estimated via EcoLogits param→energy methodology (size 200B params, "
#     "size_confidence=medium)",
#     "energy estimated from an unverified parameter count; treat this value as a "
#     "soft estimate — the EcoLogits methodology does not publish an uncertainty "
#     "band, so the range here is degenerate (min == mean == max)",
#   ),
# }
```

### Example — full RAG pipeline

```python
# Real counts from your provider SDKs:
embedding_response = client.embeddings.create(input=user_query, model="text-embedding-3-small")
chat_response      = client.chat.completions.create(
    model="gpt-4o",
    messages=full_assembled_prompt,
)

result = ci.get_co2_for_pipeline(
    [
        {
            "model_name":    "text-embedding-3-small",
            "input_tokens":  embedding_response.usage.prompt_tokens,
            "output_tokens": 0,                                    # ignored anyway for embeddings
        },
        {
            "model_name":    "gpt-4o",
            "input_tokens":  chat_response.usage.prompt_tokens,    # FULL assembled prompt
            "output_tokens": chat_response.usage.completion_tokens,
        },
    ],
    region="FR",
)

float(result)                # 0.0115 g CO2eq for the whole request
result.detail["range_method"]    # "naive_sum"
result.detail["steps"]            # list of per-step .detail dicts
```

### Energy resolution — measured vs estimated vs unknown

Each result's `energy_source_confidence` is one of:

- **`"measured"`** — energy from
  [AI Energy Score](https://huggingface.co/spaces/AIEnergyScore) (benchmark
  data, when the model is in the dataset). `energy_source ==
  "ai_energy_score"`. `modelsize_confidence.used_in_calculation == False`
  (size didn't feed the number). The shipped `data/ai_energy_score.json` is
  currently an empty stub; will be populated by the weekly build hook.
- **`"estimated"`** — energy via the
  [EcoLogits methodology](https://ecologits.ai/latest/methodology/) (param→energy
  function), reimplemented in `energy.py` (no `ecologits` runtime dependency).
  `energy_source == "ecologits_estimate"`.
  `modelsize_confidence.used_in_calculation == True`. The two real RAG
  targets (`gpt-4o` and `text-embedding-3-small`) currently fall here.
- **`"unknown"`** — no AI Energy Score entry AND `size_billion_params` is
  `None`. `get_co2_for_*` raises `ValueError` rather than producing a
  silent guess.

### Pipeline uncertainty band combination

`get_co2_for_pipeline` combines per-step uncertainty bands by **naive sum**:
`total_min = Σ step_min`, `total_max = Σ step_max`. This implicitly assumes
the steps' errors are perfectly correlated — pessimistic in the sense that
it overestimates combined uncertainty. We pick it on purpose for v1 as a
conservative upper bound; better to over-report uncertainty than to
under-report it. The statistically correct method for independent errors is
**sum-in-quadrature on the half-widths**:
`half_total = sqrt(Σ ((max_i − min_i) / 2)²)`. The combination is isolated
in `_combine_step_ranges` in `rakam_frugal_ai/co2.py`, so switching is a
one-function change. Every pipeline result carries
`result.detail["range_method"] == "naive_sum"` so it is self-describing.

## Freshness

There is **no `date=` parameter.** Electricity Maps without a key provides
no usable historical data, so we'd be accepting an arg we couldn't honor.
Instead, freshness is the build's responsibility — the wheel is republished
weekly with refreshed data. Inspect via:

```python
ci.__version__       # e.g. "2026.6.15.2" — CalVer
ci.__data_date__     # date the bundled data was fetched
```

## Single source of truth

`data/zone_ci.json` is the authoritative per-zone CI table. The model-level
`carbon_intensity` field in `data/models.json` is **derived** from that
table at build time (and re-derived at load time as a consistency check).
Per-region CI is no longer persisted on disk — it's back-filled in memory
when the registry loads. Net effect: no zone's CI is independently fetched
twice.

## Scope

The library does **not**:

- Provide live network lookups at call time (everything reads frozen data
  bundled in the wheel).
- Account for datacenter PUE, network transit, training amortization, or
  embodied hardware emissions.
- Replace dedicated tools like
  [CodeCarbon](https://mlco2.github.io/codecarbon/) for measuring your own
  inference runs — it gives you a model-aware figure when you don't have
  power telemetry.

## Python support

Python 3.10, 3.11, 3.12.

## License

MIT — see [LICENSE](LICENSE).
