Spaces:
Sleeping
Sleeping
File size: 5,214 Bytes
929f2ac aeb8c1c 929f2ac aeb8c1c 929f2ac aeb8c1c 929f2ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | """
LLM wrapper with token accounting and CO2 emission estimation.
Wraps an Azure OpenAI-compatible chat/completions call and returns:
- content: the generated text
- tokens: prompt / completion / cached / total
- energy_kwh, co2_grams: environmental impact for the call
CO2 emissions are estimated with `ecologits`, which uses a model registry
(parameter counts, hardware assumptions) plus output token count and request
latency to compute global warming potential (kgCO2eq) and energy (kWh).
We chose `ecologits` over `codecarbon` because the LLM runs on a remote
endpoint — `codecarbon` measures local process energy and would only count
the client overhead, not the inference itself.
"""
import logging
import time
from typing import Optional
import requests
from fastapi import HTTPException
logger = logging.getLogger(__name__)
try:
from ecologits.tracers.utils import llm_impacts
_ECOLOGITS_AVAILABLE = True
except ImportError:
_ECOLOGITS_AVAILABLE = False
logger.warning("ecologits not installed — CO2 emission will be reported as None.")
def _to_scalar(value) -> Optional[float]:
"""
Normalize an ecologits impact value to a single float.
ecologits returns either a plain float or a RangeValue(min, max) depending
on the model registry entry. For RangeValue we return the midpoint so a
single representative number flows through the API/UI.
"""
if value is None:
return None
if hasattr(value, "min") and hasattr(value, "max"):
return (float(value.min) + float(value.max)) / 2.0
try:
return float(value)
except (TypeError, ValueError):
return None
def call_llm(
prompt: str,
*,
endpoint_url: str,
api_key: str,
model: str,
max_completion_tokens: int = 512,
temperature: float = 0.7,
top_p: float = 0.95,
provider: str = "openai",
timeout: Optional[float] = None,
) -> dict:
"""
Call an Azure OpenAI-compatible chat/completions endpoint and return the
response together with token counts and CO2 emission estimate.
Returns a dict:
{
"content": str,
"tokens": {
"prompt": int,
"completion": int,
"cached": int,
"total": int,
},
"energy_kwh": float | None,
"co2_grams": float | None,
"latency_s": float,
}
"""
headers = {
"api-key": api_key,
"Content-Type": "application/json",
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_completion_tokens": max_completion_tokens,
"temperature": temperature,
"top_p": top_p,
}
start = time.perf_counter()
try:
resp = requests.post(endpoint_url, headers=headers, json=payload, timeout=timeout)
resp.raise_for_status()
data = resp.json()
except requests.exceptions.HTTPError as e:
logger.error(f"LLM API call failed: {e} — {resp.text}")
raise HTTPException(status_code=503, detail=f"LLM service unavailable: {str(e)}")
except (requests.exceptions.JSONDecodeError, ValueError):
logger.error(
f"LLM API returned non-JSON response (status {resp.status_code}): {repr(resp.text)}"
)
raise HTTPException(status_code=502, detail="LLM service returned an invalid response")
latency_s = time.perf_counter() - start
try:
content = data["choices"][0]["message"]["content"].strip()
except (KeyError, IndexError) as e:
logger.error(f"Unexpected LLM response format: {e} — body: {data!r}")
raise HTTPException(status_code=502, detail="Unexpected response from LLM service")
usage = data.get("usage") or {}
prompt_tokens = usage.get("prompt_tokens", 0)
completion_tokens = usage.get("completion_tokens", 0)
total_tokens = usage.get("total_tokens", prompt_tokens + completion_tokens)
prompt_details = usage.get("prompt_tokens_details") or {}
cached_tokens = prompt_details.get("cached_tokens", usage.get("cached_tokens", 0))
energy_kwh: Optional[float] = None
co2_grams: Optional[float] = None
if _ECOLOGITS_AVAILABLE:
try:
impacts = llm_impacts(
provider=provider,
model_name=model,
output_token_count=completion_tokens,
request_latency=latency_s,
)
if impacts is not None:
energy_kwh = _to_scalar(impacts.energy.value)
# ecologits returns gwp in kgCO2eq; convert to grams
gwp_kg = _to_scalar(impacts.gwp.value)
co2_grams = gwp_kg * 1000.0 if gwp_kg is not None else None
except Exception as e:
logger.warning(f"ecologits impact calc failed for model={model}: {e}")
return {
"content": content,
"tokens": {
"prompt": prompt_tokens,
"completion": completion_tokens,
"cached": cached_tokens,
"total": total_tokens,
},
"energy_kwh": energy_kwh,
"co2_grams": co2_grams,
"latency_s": latency_s,
}
|