From 85cf53024ece845682bdbdeddbfcd50c322150c4 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Wed, 17 Jun 2026 20:39:07 +0200 Subject: [PATCH 1/5] chore: upgrade ecologits to 0.11.0 --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a6a18fa..a886a53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Api to use ecologits solution" requires-python = ">=3.10,<4" dependencies = [ "fastapi[standard]>=0.128.4,<1.0.0", - "ecologits>=0.10.2,<1.0.0", + "ecologits>=0.11.0,<1.0.0", "pydantic-settings>=2.12.0", "ruff>=0.15.6", ] diff --git a/uv.lock b/uv.lock index abd52d4..ad3fcc1 100644 --- a/uv.lock +++ b/uv.lock @@ -76,16 +76,16 @@ wheels = [ [[package]] name = "ecologits" -version = "0.10.2" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pydantic" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/7b/a8af782f0305bd08f6c61614680f9c4a70e4c30831bba435d5bab603bac4/ecologits-0.10.2.tar.gz", hash = "sha256:cdbd1f67a82d4008c6bb2d3373912f5f58fba4eedb25531972544b950bab5332", size = 1020789, upload-time = "2026-06-04T17:31:08.75Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/c7/efce45391a269c5f18060f963e5ee876c43e6d76529fa7ca447f9e3bedef/ecologits-0.11.0.tar.gz", hash = "sha256:bcd233c5b01c3cb8c98a7c1dc919668bf8528d23f667bd00769ca92d1254227b", size = 1031586, upload-time = "2026-06-17T16:02:06.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/6b/a6d8354a9816401f52f5a8a08b038cf525b40f2b4efddda41d72eb392eb4/ecologits-0.10.2-py3-none-any.whl", hash = "sha256:d98973a196566f640c8b213f371b44b6d0b11a4b49a9551d3771fee77ac798d7", size = 50769, upload-time = "2026-06-04T17:31:09.999Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d3/bc89b4a3f959eb0f14585fe384c27dd154fb8f19a569d712469a98515165/ecologits-0.11.0-py3-none-any.whl", hash = "sha256:dabbba7864b8be33c5bba025ffcffccc0302086e23b12ae67f087ce554b23506", size = 57089, upload-time = "2026-06-17T16:02:05.955Z" }, ] [[package]] @@ -107,7 +107,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "ecologits", specifier = ">=0.10.2,<1.0.0" }, + { name = "ecologits", specifier = ">=0.11.0,<1.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.128.4,<1.0.0" }, { name = "pydantic-settings", specifier = ">=2.12.0" }, { name = "ruff", specifier = ">=0.15.6" }, From b735d059c50eb1b47d960484f971ba45183f42b8 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Wed, 17 Jun 2026 20:40:48 +0200 Subject: [PATCH 2/5] feat: add video estimations, providers and models endpoints --- app/api/v1beta/responses/__init__.py | 15 ++- app/api/v1beta/responses/estimations.py | 61 ++++++++++++ app/api/v1beta/responses/models.py | 29 ++++++ app/api/v1beta/responses/providers.py | 18 ++++ app/api/v1beta/router.py | 121 +++++++++++++++++++++++- app/core/description.md | 6 ++ app/main.py | 8 ++ 7 files changed, 254 insertions(+), 4 deletions(-) diff --git a/app/api/v1beta/responses/__init__.py b/app/api/v1beta/responses/__init__.py index 471102f..44a515f 100644 --- a/app/api/v1beta/responses/__init__.py +++ b/app/api/v1beta/responses/__init__.py @@ -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", ] diff --git a/app/api/v1beta/responses/estimations.py b/app/api/v1beta/responses/estimations.py index 4393937..456048f 100644 --- a/app/api/v1beta/responses/estimations.py +++ b/app/api/v1beta/responses/estimations.py @@ -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, + } + } + } + }, + }, +} diff --git a/app/api/v1beta/responses/models.py b/app/api/v1beta/responses/models.py index 2380f84..26943d5 100644 --- a/app/api/v1beta/responses/models.py +++ b/app/api/v1beta/responses/models.py @@ -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"}} + }, + }, +} diff --git a/app/api/v1beta/responses/providers.py b/app/api/v1beta/responses/providers.py index 7ca24f5..96da053 100644 --- a/app/api/v1beta/responses/providers.py +++ b/app/api/v1beta/responses/providers.py @@ -17,3 +17,21 @@ }, }, } + +VIDEO_PROVIDERS_RESPONSES = { + 200: { + "description": "List of video generation provider identifiers.", + "content": { + "application/json": { + "example": { + "providers": [ + "klingai", + "openai", + "google", + "runway", + ] + } + } + }, + }, +} diff --git a/app/api/v1beta/router.py b/app/api/v1beta/router.py index 357560b..946921d 100644 --- a/app/api/v1beta/router.py +++ b/app/api/v1beta/router.py @@ -1,6 +1,11 @@ +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 ( @@ -8,11 +13,27 @@ 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"], + } + + @api_router_v1beta.get( "/providers", response_model=dict, @@ -59,6 +80,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, @@ -141,3 +208,55 @@ def post_estimations( 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") diff --git a/app/core/description.md b/app/core/description.md index 2128964..e84b41d 100644 --- a/app/core/description.md +++ b/app/core/description.md @@ -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? @@ -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/) diff --git a/app/main.py b/app/main.py index 89c5711..9716434 100644 --- a/app/main.py +++ b/app/main.py @@ -10,11 +10,19 @@ "name": "Estimations", "description": "Estimate the environmental impacts of an LLM inference request.", }, + { + "name": "Video estimations", + "description": "Estimate the environmental impacts of a video generation request.", + }, { "name": "Catalog", "description": "Browse the AI providers and models supported by EcoLogits. " "Use these endpoints to discover valid values for the estimation request.", }, + { + "name": "Video catalog", + "description": "Browse the video generation providers and models supported by EcoLogits.", + }, { "name": "Electricity mix", "description": "Retrieve the electricity mix composition for a given geographic zone. " From 2032f58306dff6fc983365112220b6ddece383a5 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Wed, 17 Jun 2026 20:42:51 +0200 Subject: [PATCH 3/5] refactor: deprecate electricity_mix_zone and use datacenter_location --- app/api/v1beta/router.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/api/v1beta/router.py b/app/api/v1beta/router.py index 946921d..c9b19f6 100644 --- a/app/api/v1beta/router.py +++ b/app/api/v1beta/router.py @@ -186,11 +186,18 @@ 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: @@ -202,7 +209,7 @@ 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} From 46d4f6f48e839ce4b363801c4ebf416d9a571f90 Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Wed, 17 Jun 2026 20:43:27 +0200 Subject: [PATCH 4/5] refactor: harmonize model_name including ai publisher e.g. "openai/gpt-5" --- app/api/v1beta/router.py | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/app/api/v1beta/router.py b/app/api/v1beta/router.py index c9b19f6..631d2a5 100644 --- a/app/api/v1beta/router.py +++ b/app/api/v1beta/router.py @@ -34,6 +34,19 @@ def _public_video_model(model: dict) -> dict: } +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, @@ -162,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( ..., @@ -201,6 +214,19 @@ def post_estimations( ), ): 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, From b75d3e7f0986fe98b6754f22d0f94a40b7d530db Mon Sep 17 00:00:00 2001 From: Samuel Rince Date: Wed, 17 Jun 2026 20:43:41 +0200 Subject: [PATCH 5/5] test: add and update tests --- tests/v1beta/test_router.py | 248 +++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 2 deletions(-) diff --git a/tests/v1beta/test_router.py b/tests/v1beta/test_router.py index a588ea4..02d8189 100644 --- a/tests/v1beta/test_router.py +++ b/tests/v1beta/test_router.py @@ -1,3 +1,6 @@ +from ecologits.status_messages import ModelNotRegisteredError +from ecologits.tracers.utils import ImpactsOutput +from ecologits.estimations.video import video_impacts from ecologits.model_repository import Providers from ecologits.tracers.utils import llm_impacts from fastapi.testclient import TestClient @@ -27,6 +30,35 @@ def test_get_models_invalid_provider(): assert response.json() == {"detail": "Provider not found"} +def test_get_video_providers(): + response = client.get("/v1beta/video-providers") + assert response.status_code == 200 + assert "providers" in response.json() + assert "openai" in response.json()["providers"] + assert "google" in response.json()["providers"] + + +def test_get_video_models_valid_provider(): + response = client.get("/v1beta/video-models/google") + assert response.status_code == 200 + assert "models" in response.json() + assert { + "provider": "google", + "model_name": "google/veo-3.1", + "capabilities": { + "resolutions": [[1280, 720], [1920, 1080]], + "frames_count": [97, 145, 193], + "audio_generation": True, + }, + } in response.json()["models"] + + +def test_get_video_models_invalid_provider(): + response = client.get("/v1beta/video-models/invalid_provider") + assert response.status_code == 404 + assert response.json() == {"detail": "Video provider not found"} + + def test_find_electricity_mix_zones_valid(): """Test the GET /electricity-mix-zones/{zone} endpoint with a valid zone""" response = client.get("/v1beta/electricity-mix-zones/WOR") @@ -94,13 +126,12 @@ def test_post_estimations(): def test_post_estimations_default_electricity_mix(): - """Test the POST /estimations endpoint without electricity_mix_zone (should use default)""" + """Test the POST /estimations endpoint without datacenter_location""" payload = { "provider": "openai", "model_name": "gpt-4o-mini", "output_token_count": 150, "request_latency": 0.8, - # electricity_mix_zone not provided, should default to "WOR" } # Get the expected impacts directly from llm_impacts @@ -123,6 +154,155 @@ def test_post_estimations_default_electricity_mix(): assert response_data["impacts"] == expected_impacts_dict +def test_post_estimations_datacenter_location(): + """Test the POST /estimations endpoint with datacenter_location""" + payload = { + "provider": "openai", + "model_name": "gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + "datacenter_location": "USA", + } + + expected_impacts = llm_impacts( + provider=payload["provider"], + model_name=payload["model_name"], + output_token_count=payload["output_token_count"], + request_latency=payload["request_latency"], + electricity_mix_zone=payload["datacenter_location"], + ) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_estimations_datacenter_location_takes_precedence(): + """Test datacenter_location takes precedence over deprecated electricity_mix_zone""" + payload = { + "provider": "openai", + "model_name": "gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + "datacenter_location": "USA", + "electricity_mix_zone": "WOR", + } + + expected_impacts = llm_impacts( + provider=payload["provider"], + model_name=payload["model_name"], + output_token_count=payload["output_token_count"], + request_latency=payload["request_latency"], + electricity_mix_zone=payload["datacenter_location"], + ) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_estimations_deprecated_electricity_mix_zone(): + """Test deprecated electricity_mix_zone still works""" + payload = { + "provider": "openai", + "model_name": "gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + "electricity_mix_zone": "USA", + } + + expected_impacts = llm_impacts( + provider=payload["provider"], + model_name=payload["model_name"], + output_token_count=payload["output_token_count"], + request_latency=payload["request_latency"], + electricity_mix_zone=payload["electricity_mix_zone"], + ) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_estimations_provider_qualified_model_name(): + """Test the POST /estimations endpoint with provider inferred from model_name""" + payload = { + "model_name": "openai/gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + } + + expected_impacts = llm_impacts( + provider="openai", + model_name="gpt-5", + output_token_count=payload["output_token_count"], + request_latency=payload["request_latency"], + ) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_estimations_provider_and_short_model_name(): + """Test the POST /estimations endpoint with explicit provider and short model name""" + payload = { + "provider": "openai", + "model_name": "gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + } + + expected_impacts = llm_impacts( + provider=payload["provider"], + model_name=payload["model_name"], + output_token_count=payload["output_token_count"], + request_latency=payload["request_latency"], + ) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_estimations_unqualified_model_name_without_provider(): + """Test the POST /estimations endpoint when provider cannot be inferred""" + payload = { + "model_name": "gpt-5", + "output_token_count": 150, + "request_latency": 0.8, + } + error = ModelNotRegisteredError( + message=( + "Could not infer provider from model_name. Use a provider-qualified " + "model name such as `openai/gpt-5`, or provide `provider`." + ) + ) + expected_impacts = ImpactsOutput(errors=[error]) + + response = client.post("/v1beta/estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] == expected_impacts.model_dump() + + def test_post_estimations_missing_required_fields(): """Test the POST /estimations endpoint with missing required fields""" payload = { @@ -133,3 +313,67 @@ def test_post_estimations_missing_required_fields(): response = client.post("/v1beta/estimations", json=payload) assert response.status_code == 422 # Unprocessable Entity for validation errors + + +def test_post_video_estimations(): + """Test the POST /video-estimations endpoint""" + payload = { + "model_name": "google/veo-3.1", + "resolution": "720p", + "duration": 4, + "with_audio": True, + "datacenter_location": "WOR", + } + + expected_impacts = video_impacts( + model_name=payload["model_name"], + resolution=payload["resolution"], + duration=payload["duration"], + with_audio=payload["with_audio"], + datacenter_location=payload["datacenter_location"], + ) + + response = client.post("/v1beta/video-estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] is not None + + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_video_estimations_default_optional_fields(): + """Test the POST /video-estimations endpoint with optional fields omitted""" + payload = { + "model_name": "google/veo-3.1", + "resolution": "720p", + "duration": 4, + } + + expected_impacts = video_impacts( + model_name=payload["model_name"], + resolution=payload["resolution"], + duration=payload["duration"], + datacenter_location=None, + ) + + response = client.post("/v1beta/video-estimations", json=payload) + assert response.status_code == 200 + + response_data = response.json() + assert "impacts" in response_data + assert response_data["impacts"] is not None + + assert response_data["impacts"] == expected_impacts.model_dump() + + +def test_post_video_estimations_missing_required_fields(): + """Test the POST /video-estimations endpoint with missing required fields""" + payload = { + "model_name": "google/veo-3.1", + # Missing resolution and duration + } + + response = client.post("/v1beta/video-estimations", json=payload) + assert response.status_code == 422