288 lines
9.9 KiB
Python
288 lines
9.9 KiB
Python
"""Provider가 비용을 반환하지 않을 때 쓰는 공식 참조단가 계산.
|
|
|
|
``cost_usd``를 직접 반환하는 Claude CLI는 SDK가 계산한 호출별 추정값을 그대로
|
|
기록한다. Codex/Agy처럼 토큰만 반환하는 구독형 CLI와 Anthropic Messages API는
|
|
호출 시점의 공식 공개 단가로 USD 상당액을 계산한다. 어느 쪽도 청구서 실비가
|
|
아니며 비교·예산 관측용 추정 비용이다.
|
|
"""
|
|
|
|
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"
|
|
OPENAI_CREDIT_VALUE_URL = (
|
|
"https://help.openai.com/en/articles/20001147-codex-credits-for-students"
|
|
)
|
|
ANTHROPIC_PRICING_URL = "https://platform.claude.com/docs/en/about-claude/pricing"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ModelRate:
|
|
input_usd_per_million: float
|
|
output_usd_per_million: float
|
|
cached_input_usd_per_million: float
|
|
rate_id: str
|
|
label: str
|
|
source_url: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CostEstimate:
|
|
cost_usd: float
|
|
rate_id: str
|
|
rate_label: str
|
|
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()
|
|
|
|
# Agy reasoning suffix는 같은 기반 모델의 추론 강도 선택값이다.
|
|
for suffix in ("-low", "-medium", "-high", "-thinking"):
|
|
if model_key.endswith(suffix):
|
|
model_key = model_key[: -len(suffix)]
|
|
break
|
|
|
|
if provider_key == "agy_cli":
|
|
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,
|
|
9.00,
|
|
0.15,
|
|
f"google-gemini-3.5-flash-standard@{RATE_CARD_VERSION}",
|
|
"Google Gemini 3.5 Flash 표준 단가 · 입력 $1.50/M · 캐시 $0.15/M · 출력 $9.00/M",
|
|
GOOGLE_PRICING_URL,
|
|
)
|
|
if model_key == "gemini-3.1-pro":
|
|
long_context = tokens_in > 200_000
|
|
return ModelRate(
|
|
4.00 if long_context else 2.00,
|
|
18.00 if long_context else 12.00,
|
|
0.40 if long_context else 0.20,
|
|
(
|
|
f"google-gemini-3.1-pro-standard-"
|
|
f"{'long' if long_context else 'short'}@{RATE_CARD_VERSION}"
|
|
),
|
|
(
|
|
"Google Gemini 3.1 Pro 표준 단가"
|
|
f" ({'>200K' if long_context else '≤200K'} 입력)"
|
|
),
|
|
GOOGLE_PRICING_URL,
|
|
)
|
|
if model_key == "claude-sonnet-4-6":
|
|
return ModelRate(
|
|
3.00,
|
|
15.00,
|
|
0.30,
|
|
f"anthropic-claude-sonnet-4.6-standard@{RATE_CARD_VERSION}",
|
|
"Anthropic Claude Sonnet 4.6 표준 단가 · 입력 $3/M · 캐시 $0.30/M · 출력 $15/M",
|
|
ANTHROPIC_PRICING_URL,
|
|
)
|
|
if model_key == "claude-opus-4-6":
|
|
return ModelRate(
|
|
5.00,
|
|
25.00,
|
|
0.50,
|
|
f"anthropic-claude-opus-4.6-standard@{RATE_CARD_VERSION}",
|
|
"Anthropic Claude Opus 4.6 표준 단가 · 입력 $5/M · 캐시 $0.50/M · 출력 $25/M",
|
|
ANTHROPIC_PRICING_URL,
|
|
)
|
|
|
|
if provider_key == "claude_api":
|
|
if model_key.startswith(("claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6")):
|
|
return ModelRate(
|
|
5.00,
|
|
25.00,
|
|
0.50,
|
|
f"anthropic-claude-opus-standard@{RATE_CARD_VERSION}",
|
|
"Anthropic Claude Opus 표준 단가 · 입력 $5/M · 캐시 $0.50/M · 출력 $25/M",
|
|
ANTHROPIC_PRICING_URL,
|
|
)
|
|
if model_key.startswith(("claude-sonnet-4-6", "claude-sonnet-4-5")):
|
|
return ModelRate(
|
|
3.00,
|
|
15.00,
|
|
0.30,
|
|
f"anthropic-claude-sonnet-standard@{RATE_CARD_VERSION}",
|
|
"Anthropic Claude Sonnet 표준 단가 · 입력 $3/M · 캐시 $0.30/M · 출력 $15/M",
|
|
ANTHROPIC_PRICING_URL,
|
|
)
|
|
if model_key.startswith("claude-haiku-4-5"):
|
|
return ModelRate(
|
|
1.00,
|
|
5.00,
|
|
0.10,
|
|
f"anthropic-claude-haiku-4.5-standard@{RATE_CARD_VERSION}",
|
|
"Anthropic Claude Haiku 4.5 표준 단가 · 입력 $1/M · 캐시 $0.10/M · 출력 $5/M",
|
|
ANTHROPIC_PRICING_URL,
|
|
)
|
|
|
|
if provider_key == "codex_cli":
|
|
# Codex rate card의 credit/MTok에 공식 환산값 2,500 credits=$100
|
|
# (1 credit=$0.04)을 적용한다.
|
|
codex_rates = {
|
|
"gpt-5.6-sol": (5.00, 30.00, 0.50),
|
|
"gpt-5.6-terra": (2.50, 15.00, 0.25),
|
|
"gpt-5.6-luna": (1.00, 6.00, 0.10),
|
|
"gpt-5.5": (5.00, 30.00, 0.50),
|
|
"gpt-5.4": (2.50, 15.00, 0.25),
|
|
"gpt-5.4-mini": (0.75, 4.52, 0.075),
|
|
"gpt-5.3-codex": (1.75, 14.00, 0.175),
|
|
"gpt-5.2": (1.75, 14.00, 0.175),
|
|
}
|
|
matched = next(
|
|
(
|
|
(name, values)
|
|
for name, values in codex_rates.items()
|
|
if model_key == name or model_key.startswith(f"{name}-")
|
|
),
|
|
None,
|
|
)
|
|
if matched is not None:
|
|
name, (input_rate, output_rate, cached_rate) = matched
|
|
return ModelRate(
|
|
input_rate,
|
|
output_rate,
|
|
cached_rate,
|
|
f"openai-codex-{name}-credits@{RATE_CARD_VERSION}",
|
|
(
|
|
f"OpenAI Codex {name} 크레딧 환산 · 입력 ${input_rate:g}/M"
|
|
f" · 캐시 ${cached_rate:g}/M · 출력 ${output_rate:g}/M"
|
|
),
|
|
OPENAI_CODEX_RATE_URL,
|
|
)
|
|
|
|
return None
|
|
|
|
|
|
def estimate_reference_cost(
|
|
*,
|
|
provider: str,
|
|
model: str,
|
|
tokens_in: int,
|
|
tokens_out: int,
|
|
priced_at: date | datetime | int | float | str,
|
|
cached_input_tokens: int = 0,
|
|
) -> CostEstimate | None:
|
|
"""공식 공개 단가로 USD 상당액을 계산한다.
|
|
|
|
``tokens_in``은 CLI가 반환한 전체 입력 토큰으로 보고 캐시 읽기 토큰을
|
|
차감한다. 잘못된 음수·과대 캐시 값은 0..tokens_in 범위로 제한한다.
|
|
"""
|
|
|
|
safe_input = max(0, int(tokens_in or 0))
|
|
safe_output = max(0, int(tokens_out or 0))
|
|
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,
|
|
priced_on=_pricing_date(priced_at),
|
|
)
|
|
if rate is None:
|
|
return None
|
|
uncached = safe_input - cached
|
|
cost = (
|
|
uncached * rate.input_usd_per_million
|
|
+ cached * rate.cached_input_usd_per_million
|
|
+ safe_output * rate.output_usd_per_million
|
|
) / MILLION
|
|
return CostEstimate(
|
|
cost_usd=round(cost, 8),
|
|
rate_id=rate.rate_id,
|
|
rate_label=rate.label,
|
|
source_url=rate.source_url,
|
|
)
|
|
|
|
|
|
def provider_uses_reference_cost(provider: str) -> bool:
|
|
"""청구 실비 대신 토큰 참조단가로 비용을 정규화하는 provider인지 반환한다."""
|
|
|
|
return provider.strip().lower() in {"agy_cli", "codex_cli", "claude_api"}
|
|
|
|
|
|
__all__ = [
|
|
"ANTHROPIC_PRICING_URL",
|
|
"CostEstimate",
|
|
"GOOGLE_PRICING_URL",
|
|
"OPENAI_CODEX_RATE_URL",
|
|
"OPENAI_CREDIT_VALUE_URL",
|
|
"RATE_CARD_VERSION",
|
|
"estimate_reference_cost",
|
|
"provider_uses_reference_cost",
|
|
]
|