관리자 사용량 근거를 정교화
This commit is contained in:
parent
ccdcfcd2f5
commit
707dba4f8f
10 changed files with 1136 additions and 209 deletions
|
|
@ -9,10 +9,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
|
||||
MILLION = 1_000_000
|
||||
RATE_CARD_VERSION = "2026-07-31"
|
||||
GOOGLE_36_RELEASE_START = date(2026, 7, 21)
|
||||
GOOGLE_FLASH_INTRO_START = date(2026, 8, 13)
|
||||
GOOGLE_FLASH_INTRO_END = date(2027, 1, 1)
|
||||
|
||||
GOOGLE_PRICING_URL = "https://ai.google.dev/gemini-api/docs/pricing"
|
||||
OPENAI_CODEX_RATE_URL = "https://help.openai.com/en/articles/20001106-codex-rate-card"
|
||||
|
|
@ -40,11 +44,65 @@ class CostEstimate:
|
|||
source_url: str
|
||||
|
||||
|
||||
def _pricing_date(value: date | datetime | int | float | str) -> date:
|
||||
if isinstance(value, datetime):
|
||||
if value.tzinfo is not None:
|
||||
value = value.astimezone(timezone.utc)
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return datetime.fromtimestamp(value, timezone.utc).date()
|
||||
return date.fromisoformat(value[:10])
|
||||
|
||||
|
||||
def _google_flash_rate(*, model_key: str, priced_on: date) -> ModelRate | None:
|
||||
if model_key == "gemini-3.6-flash" and priced_on < GOOGLE_36_RELEASE_START:
|
||||
return None
|
||||
if model_key == "gemini-3.7-flash" and priced_on < GOOGLE_FLASH_INTRO_START:
|
||||
return None
|
||||
if model_key not in {"gemini-3.6-flash", "gemini-3.7-flash"}:
|
||||
return None
|
||||
|
||||
model_name = "Gemini 3.7 Flash" if model_key == "gemini-3.7-flash" else "Gemini 3.6 Flash"
|
||||
if GOOGLE_FLASH_INTRO_START <= priced_on < GOOGLE_FLASH_INTRO_END:
|
||||
return ModelRate(
|
||||
0.75,
|
||||
3.75,
|
||||
0.075,
|
||||
f"google-{model_key}-standard-intro@2026-08-13",
|
||||
(
|
||||
f"Google {model_name} 프로모션 표준 단가(2026-12-31까지)"
|
||||
" · 입력 $0.75/M · 캐시 $0.075/M · 출력 $3.75/M"
|
||||
),
|
||||
GOOGLE_PRICING_URL,
|
||||
)
|
||||
|
||||
effective_from = "2027-01-01" if priced_on >= GOOGLE_FLASH_INTRO_END else "2026-07-21"
|
||||
period_label = (
|
||||
"2027-01-01부터"
|
||||
if priced_on >= GOOGLE_FLASH_INTRO_END
|
||||
else "2026-07-21~2026-08-12"
|
||||
)
|
||||
return ModelRate(
|
||||
1.50,
|
||||
7.50,
|
||||
0.15,
|
||||
f"google-{model_key}-standard@{effective_from}",
|
||||
(
|
||||
f"Google {model_name} 표준 단가({period_label})"
|
||||
" · 입력 $1.50/M · 캐시 $0.15/M · 출력 $7.50/M"
|
||||
),
|
||||
GOOGLE_PRICING_URL,
|
||||
)
|
||||
|
||||
|
||||
def _rate(
|
||||
*,
|
||||
provider: str,
|
||||
model: str,
|
||||
tokens_in: int,
|
||||
priced_on: date,
|
||||
) -> ModelRate | None:
|
||||
provider_key = provider.strip().lower()
|
||||
model_key = model.strip().lower()
|
||||
|
|
@ -56,15 +114,9 @@ def _rate(
|
|||
break
|
||||
|
||||
if provider_key == "agy_cli":
|
||||
if model_key == "gemini-3.6-flash":
|
||||
return ModelRate(
|
||||
1.50,
|
||||
7.50,
|
||||
0.15,
|
||||
f"google-gemini-3.6-flash-standard@{RATE_CARD_VERSION}",
|
||||
"Google Gemini 3.6 Flash 표준 단가 · 입력 $1.50/M · 캐시 $0.15/M · 출력 $7.50/M",
|
||||
GOOGLE_PRICING_URL,
|
||||
)
|
||||
flash_rate = _google_flash_rate(model_key=model_key, priced_on=priced_on)
|
||||
if flash_rate is not None:
|
||||
return flash_rate
|
||||
if model_key == "gemini-3.5-flash":
|
||||
return ModelRate(
|
||||
1.50,
|
||||
|
|
@ -182,6 +234,7 @@ def estimate_reference_cost(
|
|||
model: str,
|
||||
tokens_in: int,
|
||||
tokens_out: int,
|
||||
priced_at: date | datetime | int | float | str,
|
||||
cached_input_tokens: int = 0,
|
||||
) -> CostEstimate | None:
|
||||
"""공식 공개 단가로 USD 상당액을 계산한다.
|
||||
|
|
@ -195,7 +248,12 @@ def estimate_reference_cost(
|
|||
if safe_input == 0 and safe_output == 0:
|
||||
return None
|
||||
cached = min(safe_input, max(0, int(cached_input_tokens or 0)))
|
||||
rate = _rate(provider=provider, model=model, tokens_in=safe_input)
|
||||
rate = _rate(
|
||||
provider=provider,
|
||||
model=model,
|
||||
tokens_in=safe_input,
|
||||
priced_on=_pricing_date(priced_at),
|
||||
)
|
||||
if rate is None:
|
||||
return None
|
||||
uncached = safe_input - cached
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ def build_model_cost_report(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|||
by_provider = list(usage.get("by_provider") or [])
|
||||
budget = dict(usage.get("budget") or {})
|
||||
evaluator_cache = dict(usage.get("evaluator_cache") or {})
|
||||
total_cost_basis = str(usage.get("cost_basis") or "provider_reported")
|
||||
exact_cost_bases = {"provider_estimate", "provider_reported", "reference_rate"}
|
||||
|
||||
models: list[dict[str, Any]] = []
|
||||
for item in by_provider:
|
||||
|
|
@ -87,6 +89,12 @@ def build_model_cost_report(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|||
_number(row.get("recorded_cost_usd"), row_cost), 6
|
||||
)
|
||||
row_estimated_cost = round(_number(row.get("estimated_cost_usd")), 6)
|
||||
row_cost_basis = str(row.get("cost_basis") or "provider_reported")
|
||||
row_cost_complete = row_cost_basis not in {
|
||||
"partial",
|
||||
"partial_upper_bound",
|
||||
"unavailable",
|
||||
}
|
||||
models.append(
|
||||
{
|
||||
"provider": str(row.get("provider") or "unknown"),
|
||||
|
|
@ -100,13 +108,24 @@ def build_model_cost_report(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"cost_usd": row_cost,
|
||||
"recorded_cost_usd": row_recorded_cost,
|
||||
"estimated_cost_usd": row_estimated_cost,
|
||||
"cost_basis": str(row.get("cost_basis") or "provider_reported"),
|
||||
"cost_basis": row_cost_basis,
|
||||
"rate_label": row.get("rate_label"),
|
||||
"rate_source_url": row.get("rate_source_url"),
|
||||
"cost_share": _ratio(row_cost, total_cost),
|
||||
"cost_share": (
|
||||
_ratio(row_cost, total_cost)
|
||||
if total_cost_basis in exact_cost_bases
|
||||
and row_cost_basis in exact_cost_bases
|
||||
else None
|
||||
),
|
||||
"token_share": _ratio(float(row_tokens), float(total_tokens)),
|
||||
"cost_per_turn_usd": _cost_per_turn(row_cost, turns),
|
||||
"cost_per_1k_tokens_usd": _cost_per_1k_tokens(row_cost, row_tokens),
|
||||
"cost_per_turn_usd": (
|
||||
_cost_per_turn(row_cost, turns) if row_cost_complete else None
|
||||
),
|
||||
"cost_per_1k_tokens_usd": (
|
||||
_cost_per_1k_tokens(row_cost, row_tokens)
|
||||
if row_cost_complete
|
||||
else None
|
||||
),
|
||||
}
|
||||
)
|
||||
models.sort(key=lambda item: (-float(item["cost_usd"]), item["provider"], item["model"]))
|
||||
|
|
@ -120,15 +139,28 @@ def build_model_cost_report(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|||
warnings.append("zero_cost_metered_usage")
|
||||
if estimated_cost > 0:
|
||||
warnings.append("reference_rate_cost")
|
||||
if any(item["cost_basis"] == "unavailable" for item in models):
|
||||
if any(item["cost_basis"] == "partial" for item in models):
|
||||
warnings.append("partial_model_cost")
|
||||
if any(item["cost_basis"] == "partial_upper_bound" for item in models):
|
||||
warnings.append("partial_upper_bound_model_cost")
|
||||
if any(item["cost_basis"] == "reference_upper_bound" for item in models):
|
||||
warnings.append("reference_upper_bound_cost")
|
||||
if any(
|
||||
item["cost_basis"] in {"partial", "partial_upper_bound", "unavailable"}
|
||||
for item in models
|
||||
):
|
||||
warnings.append("unavailable_model_cost")
|
||||
if str(budget.get("status") or "") in {"warn", "exceeded"}:
|
||||
if str(budget.get("status") or "") in {"warn", "exceeded", "indeterminate"}:
|
||||
warnings.append(f"budget_{budget.get('status')}")
|
||||
cache_hit_rate = _number(evaluator_cache.get("hit_rate"))
|
||||
if bool(evaluator_cache.get("enabled")) and _integer(evaluator_cache.get("requests")) > 0:
|
||||
if cache_hit_rate < 0.25:
|
||||
warnings.append("low_evaluator_cache_hit_rate")
|
||||
if models and models[0]["cost_share"] >= 0.8:
|
||||
if (
|
||||
models
|
||||
and isinstance(models[0]["cost_share"], (float, int))
|
||||
and models[0]["cost_share"] >= 0.8
|
||||
):
|
||||
warnings.append("dominant_model_cost")
|
||||
|
||||
return {
|
||||
|
|
@ -152,14 +184,28 @@ def build_model_cost_report(usage: Mapping[str, Any]) -> dict[str, Any]:
|
|||
"cost_usd": total_cost,
|
||||
"recorded_cost_usd": recorded_cost,
|
||||
"estimated_cost_usd": estimated_cost,
|
||||
"cost_per_turn_usd": _cost_per_turn(total_cost, metered_turns),
|
||||
"cost_per_1k_tokens_usd": _cost_per_1k_tokens(total_cost, total_tokens),
|
||||
"cost_basis": total_cost_basis,
|
||||
"cost_per_turn_usd": (
|
||||
_cost_per_turn(total_cost, metered_turns)
|
||||
if total_cost_basis not in {"partial", "partial_upper_bound", "unavailable"}
|
||||
else None
|
||||
),
|
||||
"cost_per_1k_tokens_usd": (
|
||||
_cost_per_1k_tokens(total_cost, total_tokens)
|
||||
if total_cost_basis not in {"partial", "partial_upper_bound", "unavailable"}
|
||||
else None
|
||||
),
|
||||
},
|
||||
"budget": {
|
||||
"status": str(budget.get("status") or "disabled"),
|
||||
"limit_usd": round(_number(budget.get("limit_usd")), 6),
|
||||
"used_ratio": round(_number(budget.get("used_ratio")), 6),
|
||||
"remaining_usd": round(_number(budget.get("remaining_usd")), 6),
|
||||
"remaining_usd": (
|
||||
round(_number(budget.get("remaining_usd")), 6)
|
||||
if budget.get("remaining_usd") is not None
|
||||
else None
|
||||
),
|
||||
"cost_basis": str(budget.get("cost_basis") or total_cost_basis),
|
||||
},
|
||||
"evaluator_cache": {
|
||||
"enabled": bool(evaluator_cache.get("enabled")),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue