Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions app/api/v1beta/responses/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
from app.api.v1beta.responses.electricity_mix import ELECTRICITY_MIX_RESPONSES
from app.api.v1beta.responses.estimations import ESTIMATIONS_RESPONSES
from app.api.v1beta.responses.models import MODELS_RESPONSES
from app.api.v1beta.responses.providers import PROVIDERS_RESPONSES
from app.api.v1beta.responses.estimations import (
ESTIMATIONS_RESPONSES,
VIDEO_ESTIMATIONS_RESPONSES,
)
from app.api.v1beta.responses.models import MODELS_RESPONSES, VIDEO_MODELS_RESPONSES
from app.api.v1beta.responses.providers import (
PROVIDERS_RESPONSES,
VIDEO_PROVIDERS_RESPONSES,
)

__all__ = [
"PROVIDERS_RESPONSES",
"MODELS_RESPONSES",
"ELECTRICITY_MIX_RESPONSES",
"ESTIMATIONS_RESPONSES",
"VIDEO_ESTIMATIONS_RESPONSES",
"VIDEO_MODELS_RESPONSES",
"VIDEO_PROVIDERS_RESPONSES",
]
61 changes: 61 additions & 0 deletions app/api/v1beta/responses/estimations.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,64 @@
},
},
}

VIDEO_ESTIMATIONS_RESPONSES = {
200: {
"description": "Video generation environmental impact estimation with min/max intervals.",
"content": {
"application/json": {
"example": {
"impacts": {
"energy": {
"type": "energy",
"name": "Energy",
"value": {
"min": 0.18035949832689935,
"max": 0.3066421823877012,
},
"unit": "kWh",
},
"gwp": {
"type": "GWP",
"name": "Global Warming Potential",
"value": {
"min": 0.09286706406226485,
"max": 0.15074115534048974,
},
"unit": "kgCO2eq",
},
"adpe": {
"type": "ADPe",
"name": "Abiotic Depletion Potential (elements)",
"value": {
"min": 5.656388246092277e-7,
"max": 5.673423780172079e-7,
},
"unit": "kgSbeq",
},
"pe": {
"type": "PE",
"name": "Primary Energy",
"value": {
"min": 0.5207381363479325,
"max": 0.847444068281633,
},
"unit": "MJ",
},
"wcf": {
"type": "WCF",
"name": "Water Consumption Footprint",
"value": {
"min": 0.7903653815848607,
"max": 1.3437571502533046,
},
"unit": "L",
},
"warnings": None,
"errors": None,
}
}
}
},
},
}
29 changes: 29 additions & 0 deletions app/api/v1beta/responses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,32 @@
"content": {"application/json": {"example": {"detail": "Provider not found"}}},
},
}

VIDEO_MODELS_RESPONSES = {
200: {
"description": "List of video generation models for the provider.",
"content": {
"application/json": {
"example": {
"models": [
{
"provider": "google",
"model_name": "google/veo-3.1",
"capabilities": {
"resolutions": [[1280, 720], [1920, 1080]],
"frames_count": [97, 145, 193],
"audio_generation": True,
},
}
]
}
}
},
},
404: {
"description": "Video provider not found.",
"content": {
"application/json": {"example": {"detail": "Video provider not found"}}
},
},
}
18 changes: 18 additions & 0 deletions app/api/v1beta/responses/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,21 @@
},
},
}

VIDEO_PROVIDERS_RESPONSES = {
200: {
"description": "List of video generation provider identifiers.",
"content": {
"application/json": {
"example": {
"providers": [
"klingai",
"openai",
"google",
"runway",
]
}
}
},
},
}
168 changes: 160 additions & 8 deletions app/api/v1beta/router.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
import json
from importlib import resources

from ecologits.electricity_mix_repository import electricity_mixes
from ecologits.estimations.video import video_impacts
from ecologits.model_repository import Providers, models
from ecologits.tracers.utils import llm_impacts
from ecologits.status_messages import ModelNotRegisteredError
from ecologits.tracers.utils import ImpactsOutput, llm_impacts
from fastapi import APIRouter, Body, HTTPException

from app.api.v1beta.responses import (
ELECTRICITY_MIX_RESPONSES,
ESTIMATIONS_RESPONSES,
MODELS_RESPONSES,
PROVIDERS_RESPONSES,
VIDEO_ESTIMATIONS_RESPONSES,
VIDEO_MODELS_RESPONSES,
VIDEO_PROVIDERS_RESPONSES,
)

api_router_v1beta = APIRouter(prefix="/v1beta")


def _load_video_models() -> list[dict]:
video_models = resources.files("ecologits").joinpath("data/video_models.json")
return json.loads(video_models.read_text())["models"]


def _public_video_model(model: dict) -> dict:
return {
"provider": model["provider"],
"model_name": model["model_name"],
"capabilities": model["capabilities"],
}


def _normalize_llm_model(
provider: str | None, model_name: str
) -> tuple[str | None, str]:
if "/" not in model_name:
return provider, model_name

qualified_provider, qualified_model_name = model_name.split("/", maxsplit=1)
if provider is None or provider == qualified_provider:
return qualified_provider, qualified_model_name

return provider, model_name


@api_router_v1beta.get(
"/providers",
response_model=dict,
Expand Down Expand Up @@ -59,6 +93,52 @@ def get_models(provider_name: str):
raise HTTPException(status_code=500, detail="Failed to retrieve models")


@api_router_v1beta.get(
"/video-providers",
response_model=dict,
tags=["Video catalog"],
summary="List all supported video generation providers",
responses=VIDEO_PROVIDERS_RESPONSES,
)
def get_video_providers():
try:
providers_list = list(
dict.fromkeys(model["provider"] for model in _load_video_models())
)
return {
"providers": providers_list,
}
except Exception:
raise HTTPException(
status_code=500, detail="Failed to retrieve video providers"
)


@api_router_v1beta.get(
"/video-models/{provider_name}",
response_model=dict,
tags=["Video catalog"],
summary="List video generation models for a provider",
responses=VIDEO_MODELS_RESPONSES,
)
def get_video_models(provider_name: str):
try:
filter_model = [
_public_video_model(model)
for model in _load_video_models()
if model["provider"] == provider_name
]
except Exception:
raise HTTPException(status_code=500, detail="Failed to retrieve video models")

if not filter_model:
raise HTTPException(status_code=404, detail="Video provider not found")

return {
"models": filter_model,
}


@api_router_v1beta.get(
"/electricity-mix-zones/{zone}",
response_model=dict,
Expand Down Expand Up @@ -95,17 +175,17 @@ def get_electricity_mix_zones(zone: str):
responses=ESTIMATIONS_RESPONSES,
)
def post_estimations(
provider: str = Body(
...,
provider: str | None = Body(
default=None,
embed=True,
examples=["openai"],
description="Provider identifier (use `GET /v1beta/providers` to list valid values).",
description="Optional provider identifier. Required only when `model_name` is not provider-qualified.",
),
model_name: str = Body(
...,
embed=True,
examples=["gpt-4o-mini"],
description="Model identifier as registered in EcoLogits (use `GET /v1beta/models/{provider}` to list valid values).",
examples=["openai/gpt-5", "gpt-5"],
description="Model identifier, either provider-qualified (`openai/gpt-5`) or short when `provider` is provided (`gpt-5`).",
),
output_token_count: int = Body(
...,
Expand All @@ -119,14 +199,34 @@ def post_estimations(
examples=[1.5],
description="Measured request latency in seconds.",
),
datacenter_location: str | None = Body(
default=None,
embed=True,
examples=["USA"],
description="ISO 3166-1 alpha-3 datacenter zone code. Uses the provider default when omitted.",
),
electricity_mix_zone: str | None = Body(
default=None,
embed=True,
examples=["WOR"],
description="ISO 3166-1 alpha-3 zone code for the electricity mix. Defaults to `WOR` (world average). (use `GET /v1beta/electricity-mix-zones/{zone}` to check zone availability)",
deprecated=True,
description="Deprecated. Use `datacenter_location` instead.",
),
):
try:
provider, model_name = _normalize_llm_model(
provider=provider,
model_name=model_name,
)
if provider is None:
error = ModelNotRegisteredError(
message=(
"Could not infer provider from model_name. Use a provider-qualified "
"model name such as `openai/gpt-5`, or provide `provider`."
)
)
return {"impacts": ImpactsOutput(errors=[error])}

impacts = llm_impacts(
provider=provider,
model_name=model_name,
Expand All @@ -135,9 +235,61 @@ def post_estimations(
# the TPS and TTFT data from OpenRouter.
# TODO: remove the high value when the estimations module in EcoLogits (Python) is ready
request_latency=request_latency if request_latency is not None else 1e6,
electricity_mix_zone=electricity_mix_zone,
electricity_mix_zone=datacenter_location if datacenter_location is not None else electricity_mix_zone,
)
return {"impacts": impacts}

except Exception:
raise HTTPException(status_code=500, detail="Failed to Estimate impacts")


@api_router_v1beta.post(
"/video-estimations",
response_model=dict,
tags=["Video estimations"],
summary="Estimate environmental impacts of a video generation request",
responses=VIDEO_ESTIMATIONS_RESPONSES,
)
def post_video_estimations(
model_name: str = Body(
...,
embed=True,
examples=["google/veo-3.1"],
description="Video model identifier as registered in EcoLogits (use `GET /v1beta/video-models/{provider}` to list valid values).",
),
resolution: str = Body(
...,
embed=True,
examples=["720p", "1080p", "1920x1080"],
description="Generated video resolution.",
),
duration: float = Body(
...,
embed=True,
examples=[4],
description="Generated video duration in seconds.",
),
with_audio: bool = Body(
default=True,
embed=True,
description="Whether the generated video includes audio.",
),
datacenter_location: str | None = Body(
default=None,
embed=True,
examples=["USA"],
description="ISO 3166-1 alpha-3 datacenter zone code. Uses the provider default when omitted.",
),
):
try:
impacts = video_impacts(
model_name=model_name,
resolution=resolution,
duration=duration,
with_audio=with_audio,
datacenter_location=datacenter_location,
)
return {"impacts": impacts}

except Exception:
raise HTTPException(status_code=500, detail="Failed to estimate video impacts")
6 changes: 6 additions & 0 deletions app/core/description.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
**EcoLogits API** provides a language-agnostic HTTP interface to the
[EcoLogits](https://ecologits.ai) library, so any stack — not just Python — can
estimate the environmental footprint of generative-AI inference.
It supports LLM request estimations and video generation estimations through
separate endpoint families because their inputs and model catalogs differ.

## What is EcoLogits?

Expand All @@ -20,6 +22,10 @@ It follows Life Cycle Assessment (LCA) principles defined by ISO 14044.

Results are returned as **approximation intervals** (min/max range), not single point estimates.

Use `/v1beta/estimations` with `/v1beta/providers` and `/v1beta/models/{provider}`
for LLM inference. Use `/v1beta/video-estimations` with `/v1beta/video-providers`
and `/v1beta/video-models/{provider}` for video generation.

## Useful links

- [EcoLogits documentation](https://ecologits.ai/)
Expand Down
Loading
Loading