LLM API 비용 모니터링
Claude, GPT, Gemini API 비용을 효과적으로 추적하고 최적화하는 완벽한 가이드
업데이트 안내: 모델/요금/버전/정책 등 시점에 민감한 정보는 변동될 수 있습니다.
최신 내용은 공식 문서를 확인하세요.
⚠️ 비용 관리가 중요한 이유
- 예상치 못한 청구: 무제한 사용 시 수천~수만 달러 청구 가능
- 테스트 실수: 무한 루프나 대량 호출로 인한 비용 폭증
- 악의적 사용: API 키 유출 시 타인의 악용
- 비효율: 불필요하게 긴 컨텍스트나 큰 모델 사용
모니터링과 알림 설정은 필수입니다!
LLM 가격 구조
주요 모델 가격 (2024년 기준)
| 제공자 | 모델 | 입력 (1M 토큰) | 출력 (1M 토큰) | 컨텍스트 |
|---|---|---|---|---|
| Anthropic | Claude Opus (고성능) | 변동 | 변동 | 200K |
| Claude Sonnet (균형) | 변동 | 변동 | 200K | |
| Claude Haiku (경량) | 변동 | 변동 | 200K | |
| OpenAI | GPT-4 Turbo | 변동 | 변동 | 128K |
| GPT-4o | 변동 | 변동 | 128K | |
| GPT-4o mini | 변동 | 변동 | 128K | |
| o1-preview | 변동 | 변동 | 128K | |
| Gemini 1.5 Pro | 변동 | 변동 | 2M | |
| Gemini 1.5 Flash | 변동 | 변동 | 1M | |
| Ollama | 모든 모델 | 무료 (로컬 실행, 하드웨어 비용만) | ||
비용 계산기
def calculate_cost(
model: str,
input_tokens: int,
output_tokens: int
) -> float:
"""토큰 수로 비용 계산"""
# 가격표 (입력, 출력 per 1M tokens)
prices = {
"claude-" : (15.00, 75.00),
"claude-" : (3.00, 15.00),
"claude-" : (0.80, 4.00),
"gpt-4-turbo": (10.00, 30.00),
"gpt-4o": (2.50, 10.00),
"gpt-4o-mini": (0.15, 0.60),
"gemini-1.5-pro": (3.50, 10.50),
"gemini-1.5-flash": (0.075, 0.30),
}
if model not in prices:
raise ValueError(f"Unknown model: {model}")
input_price, output_price = prices[model]
input_cost = (input_tokens / 1_000_000) * input_price
output_cost = (output_tokens / 1_000_000) * output_price
return input_cost + output_cost
# 사용 예시
cost = calculate_cost(
model="claude-" ,
input_tokens=5000,
output_tokens=1000
)
print(f"비용: ${cost:.4f}")
# 출력: 비용: 변동
실제 사용 예시
| 작업 | 모델 | 토큰 (입력/출력) | 비용 |
|---|---|---|---|
| 간단한 질문 | Sonnet (균형) | 100 / 50 | 변동 |
| 코드 리뷰 (1파일) | Sonnet (균형) | 2,000 / 500 | 변동 |
| 문서 요약 (10페이지) | Haiku (경량) | 8,000 / 300 | 변동 |
| PR 전체 리뷰 | Sonnet (균형) | 15,000 / 3,000 | 변동 |
| 대규모 코드베이스 분석 | Opus (고성능) | 100,000 / 5,000 | 변동 |
💡 비용 절감 팁
- 작은 모델 우선: Haiku → Sonnet → Opus 순으로 시도
- 캐싱 활용: Prompt Caching으로 반복 컨텍스트 90% 절감
- 컨텍스트 최소화: 필요한 정보만 전달
- 출력 길이 제한: max_tokens 적절히 설정
- 로컬 모델: Ollama로 비용 제로화 (하드웨어 비용만)
사용량 추적
기본 로깅
Python 예제
import anthropic
import os
import json
import logging
from datetime import datetime
# 로깅 설정
logging.basicConfig(
filename='llm_usage.log',
level=logging.INFO,
format='%(asctime)s - %(message)s'
)
class CostTracker:
def __init__(self):
self.client = anthropic.Anthropic(
api_key=os.getenv("ANTHROPIC_API_KEY")
)
def log_usage(self, model, usage, cost):
"""API 사용량 로깅"""
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"model": model,
"input_tokens": usage.input_tokens,
"output_tokens": usage.output_tokens,
"cost_usd": cost,
}
logging.info(json.dumps(log_entry))
def create_message(self, model, messages, **kwargs):
"""비용 추적과 함께 메시지 생성"""
message = self.client.messages.create(
model=model,
messages=messages,
**kwargs
)
# 비용 계산
cost = calculate_cost(
model=model,
input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens
)
# 로깅
self.log_usage(model, message.usage, cost)
return message
# 사용
tracker = CostTracker()
message = tracker.create_message(
model="claude-" ,
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}]
)
데이터베이스 저장
SQLite 예제
import sqlite3
from datetime import datetime
class UsageDatabase:
def __init__(self, db_path="llm_usage.db"):
self.conn = sqlite3.connect(db_path)
self.create_table()
def create_table(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS api_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
user_id TEXT,
model TEXT NOT NULL,
input_tokens INTEGER NOT NULL,
output_tokens INTEGER NOT NULL,
cost_usd REAL NOT NULL,
request_id TEXT,
metadata TEXT
)
""")
self.conn.commit()
def log_request(
self,
model: str,
input_tokens: int,
output_tokens: int,
cost: float,
user_id: str = None,
request_id: str = None,
metadata: dict = None
):
self.conn.execute(
"""
INSERT INTO api_usage
(timestamp, user_id, model, input_tokens, output_tokens, cost_usd, request_id, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
datetime.utcnow().isoformat(),
user_id,
model,
input_tokens,
output_tokens,
cost,
request_id,
json.dumps(metadata) if metadata else None
)
)
self.conn.commit()
def get_daily_cost(self, date: str = None) -> float:
"""특정 날짜의 총 비용"""
if not date:
date = datetime.utcnow().strftime("%Y-%m-%d")
cursor = self.conn.execute(
"SELECT SUM(cost_usd) FROM api_usage WHERE DATE(timestamp) = ?",
(date,)
)
result = cursor.fetchone()[0]
return result or 0.0
def get_user_usage(self, user_id: str, days: int = 30):
"""사용자별 사용량 통계"""
cursor = self.conn.execute(
"""
SELECT
model,
COUNT(*) as requests,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(cost_usd) as total_cost
FROM api_usage
WHERE user_id = ?
AND timestamp >= datetime('now', '-' || ? || ' days')
GROUP BY model
""",
(user_id, days)
)
return cursor.fetchall()
# 사용
db = UsageDatabase()
db.log_request(
model="claude-" ,
input_tokens=5000,
output_tokens=1000,
cost=0.03,
user_id="user123"
)
print(f"Today's cost: ${db.get_daily_cost():.2f}")
API 래퍼 클래스
class MonitoredAnthropicClient:
"""비용 추적 기능이 내장된 Anthropic 클라이언트"""
def __init__(self, api_key: str, db: UsageDatabase):
self.client = anthropic.Anthropic(api_key=api_key)
self.db = db
def messages_create(
self,
user_id: str = None,
**kwargs
) -> anthropic.types.Message:
"""메시지 생성 + 자동 로깅"""
# API 호출
message = self.client.messages.create(**kwargs)
# 비용 계산
cost = calculate_cost(
model=kwargs['model'],
input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens
)
# DB 로깅
self.db.log_request(
model=kwargs['model'],
input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens,
cost=cost,
user_id=user_id,
request_id=message.id
)
return message
# 사용
db = UsageDatabase()
client = MonitoredAnthropicClient(
api_key=os.getenv("ANTHROPIC_API_KEY"),
db=db
)
message = client.messages_create(
model="claude-" ,
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
user_id="user123"
)
알림 설정
비용 임계값 알림
이메일 알림
import smtplib
from email.mime.text import MIMEText
class CostAlerter:
def __init__(
self,
daily_limit: float = 100.0,
monthly_limit: float = 1000.0,
email_to: str = "admin@example.com"
):
self.daily_limit = daily_limit
self.monthly_limit = monthly_limit
self.email_to = email_to
self.db = UsageDatabase()
def check_limits(self):
"""비용 제한 확인 및 알림"""
daily_cost = self.db.get_daily_cost()
monthly_cost = self.get_monthly_cost()
# 일일 한도 초과
if daily_cost > self.daily_limit:
self.send_alert(
subject="⚠️ Daily API cost limit exceeded",
message=f"Daily cost: ${daily_cost:.2f} (limit: ${self.daily_limit})"
)
# 월간 한도 초과
if monthly_cost > self.monthly_limit:
self.send_alert(
subject="🚨 Monthly API cost limit exceeded",
message=f"Monthly cost: ${monthly_cost:.2f} (limit: ${self.monthly_limit})"
)
# 경고 (80% 도달)
if daily_cost > self.daily_limit * 0.8:
self.send_alert(
subject="⚠️ Approaching daily limit",
message=f"Daily cost: ${daily_cost:.2f} (80% of limit)"
)
def send_alert(self, subject: str, message: str):
"""이메일 알림 전송"""
msg = MIMEText(message)
msg['Subject'] = subject
msg['From'] = 'alerts@example.com'
msg['To'] = self.email_to
with smtplib.SMTP('smtp.gmail.com', 587) as server:
server.starttls()
server.login(
os.getenv('SMTP_USER'),
os.getenv('SMTP_PASS')
)
server.send_message(msg)
def get_monthly_cost(self) -> float:
cursor = self.db.conn.execute(
"SELECT SUM(cost_usd) FROM api_usage WHERE strftime('%Y-%m', timestamp) = strftime('%Y-%m', 'now')"
)
result = cursor.fetchone()[0]
return result or 0.0
# 주기적으로 실행 (cron 또는 스케줄러)
alerter = CostAlerter(daily_limit=50.0, monthly_limit=500.0)
alerter.check_limits()
Slack 알림
import requests
def send_slack_alert(webhook_url: str, message: str):
"""Slack으로 알림 전송"""
payload = {
"text": message,
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": message
}
}
]
}
response = requests.post(webhook_url, json=payload)
response.raise_for_status()
# 사용
if daily_cost > 100:
send_slack_alert(
webhook_url=os.getenv("SLACK_WEBHOOK_URL"),
message=f"🚨 *API Cost Alert*\n\nDaily cost: ${daily_cost:.2f}\nLimit: 변동"
)
실시간 알림
class RealtimeMonitor:
def __init__(self, threshold_per_request: float = 1.0):
self.threshold = threshold_per_request
def monitor_request(self, cost: float, model: str, user_id: str):
"""단일 요청이 임계값 초과 시 즉시 알림"""
if cost > self.threshold:
self.alert_expensive_request(cost, model, user_id)
def alert_expensive_request(self, cost: float, model: str, user_id: str):
message = f"""
⚠️ Expensive API Request Detected
User: {user_id}
Model: {model}
Cost: ${cost:.2f}
Threshold: ${self.threshold:.2f}
This single request exceeded the per-request cost threshold.
Please review the usage pattern.
"""
send_slack_alert(
webhook_url=os.getenv("SLACK_WEBHOOK_URL"),
message=message
)
비용 최적화 전략
프롬프트 캐싱
💡 Prompt Caching (Claude)
반복되는 컨텍스트를 캐시하면 입력 토큰 비용을 90% 절감할 수 있습니다. 캐시는 5분간 유지되며, 1024 토큰 이상의 블록에 적용됩니다.
# 캐싱 없이 (매번 전체 비용 지불)
message = client.messages.create(
model="claude-" ,
max_tokens=1024,
system="You are a code reviewer. [긴 가이드라인...]", # 10,000 토큰
messages=[{"role": "user", "content": "Review this code..."}]
)
# 비용: 변동 (10,000 입력 토큰)
# 캐싱 사용 (첫 요청 후 90% 절감)
message = client.messages.create(
model="claude-" ,
max_tokens=1024,
system=[
{
"type": "text",
"text": "You are a code reviewer. [긴 가이드라인...]",
"cache_control": {"type": "ephemeral"} # 캐싱 활성화
}
],
messages=[{"role": "user", "content": "Review this code..."}]
)
# 첫 요청: 변동 (캐시 쓰기 비용 25% 추가)
# 이후 요청: 변동 (캐시 읽기 비용 90% 절감)
모델 선택 전략
계층적 모델 사용
def smart_generate(prompt: str, complexity: str = "auto"):
"""복잡도에 따라 적절한 모델 선택"""
if complexity == "auto":
# 간단한 휴리스틱으로 복잡도 판단
if len(prompt) < 500:
complexity = "simple"
elif "analyze" in prompt.lower() or "complex" in prompt.lower():
complexity = "complex"
else:
complexity = "medium"
# 모델 선택
model_map = {
"simple": "claude-" , # 변동/변동 per 1M
"medium": "claude-" , # 변동/변동 per 1M
"complex": "claude-" , # 변동/변동 per 1M
}
model = model_map[complexity]
message = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message
# 사용
# 간단한 작업 → Haiku (저렴)
response = smart_generate("What is 2+2?")
# 복잡한 작업 → Opus (비싸지만 정확)
response = smart_generate("Analyze this complex algorithm...", complexity="complex")
컨텍스트 최적화
def summarize_context(long_text: str, max_tokens: int = 2000) -> str:
"""긴 컨텍스트를 요약하여 토큰 절약"""
# 이미 충분히 짧으면 그대로 반환
estimated_tokens = len(long_text) / 4 # 대략적인 추정
if estimated_tokens < max_tokens:
return long_text
# Haiku로 요약 (저렴)
message = client.messages.create(
model="claude-" ,
max_tokens=max_tokens,
messages=[{
"role": "user",
"content": f"Summarize the following in {max_tokens} tokens or less:\n\n{long_text}"
}]
)
return message.content[0].text
# 사용
large_codebase = read_all_files() # 100,000 토큰
summary = summarize_context(large_codebase, max_tokens=5000) # 5,000 토큰으로 축소
# 요약된 컨텍스트로 메인 작업 수행
result = client.messages.create(
model="claude-" ,
max_tokens=2048,
messages=[{
"role": "user",
"content": f"Based on this codebase summary:\n{summary}\n\nGenerate a README."
}]
)
# 총 비용: 요약(변동) + 메인(변동) = 변동
# vs 전체 전달: 변동 (3배 절감!)
배치 처리
def batch_reviews(files: list[str]) -> list[str]:
"""여러 파일을 한 번에 리뷰하여 오버헤드 감소"""
# 개별 요청 (비효율)
# for file in files:
# review(file) # 각각 시스템 프롬프트 전송
# 배치 요청 (효율적)
combined_content = "\n\n---\n\n".join([
f"## File: {file}\n```\n{read_file(file)}\n```"
for file in files
])
message = client.messages.create(
model="claude-" ,
max_tokens=8192,
system="You are a code reviewer...", # 한 번만 전송
messages=[{
"role": "user",
"content": f"Review these {len(files)} files:\n\n{combined_content}"
}]
)
# 파일별 리뷰 파싱
reviews = message.content[0].text.split("## File:")
return reviews[1:] # 첫 번째는 빈 문자열
로컬 모델 활용
import ollama
def hybrid_generation(prompt: str, require_accuracy: bool = False):
"""간단한 작업은 Ollama, 복잡한 작업은 Claude"""
if not require_accuracy:
# Ollama 사용 (무료)
response = ollama.generate(
model='llama3.2:3b',
prompt=prompt
)
return response['response']
else:
# Claude 사용 (유료, 더 정확)
message = client.messages.create(
model="claude-" ,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
# 사용
# 간단한 질문 → Ollama (무료)
answer = hybrid_generation("Explain what a for loop is")
# 중요한 작업 → Claude (유료, 정확)
review = hybrid_generation(
"Review this security-critical code...",
require_accuracy=True
)
대시보드 구축
Streamlit 대시보드
dashboard.py
import streamlit as st
import pandas as pd
import plotly.express as px
from datetime import datetime, timedelta
st.set_page_config(page_title="LLM Cost Dashboard", layout="wide")
# 데이터 로드
db = UsageDatabase()
# 제목
st.title("🤖 LLM API Cost Dashboard")
# 날짜 범위 선택
col1, col2 = st.columns(2)
with col1:
start_date = st.date_input("Start Date", datetime.now() - timedelta(days=30))
with col2:
end_date = st.date_input("End Date", datetime.now())
# 전체 통계
st.header("📊 Overall Statistics")
cursor = db.conn.execute(
"""
SELECT
COUNT(*) as total_requests,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(cost_usd) as total_cost
FROM api_usage
WHERE DATE(timestamp) BETWEEN ? AND ?
""",
(start_date.isoformat(), end_date.isoformat())
)
stats = cursor.fetchone()
col1, col2, col3, col4 = st.columns(4)
col1.metric("Total Requests", f"{stats[0]:,}")
col2.metric("Total Tokens", f"{(stats[1] + stats[2]):,}")
col3.metric("Total Cost", f"${stats[3]:.2f}")
col4.metric("Avg Cost/Request", f"${stats[3]/max(stats[0],1):.4f}")
# 일별 비용 그래프
st.header("📈 Daily Cost Trend")
df = pd.read_sql_query(
"""
SELECT
DATE(timestamp) as date,
model,
SUM(cost_usd) as cost
FROM api_usage
WHERE DATE(timestamp) BETWEEN ? AND ?
GROUP BY DATE(timestamp), model
ORDER BY date
""",
db.conn,
params=(start_date.isoformat(), end_date.isoformat())
)
if not df.empty:
fig = px.line(
df,
x="date",
y="cost",
color="model",
title="Daily Cost by Model"
)
st.plotly_chart(fig, use_container_width=True)
# 모델별 통계
st.header("🤖 Cost by Model")
model_stats = pd.read_sql_query(
"""
SELECT
model,
COUNT(*) as requests,
SUM(cost_usd) as total_cost,
AVG(cost_usd) as avg_cost
FROM api_usage
WHERE DATE(timestamp) BETWEEN ? AND ?
GROUP BY model
ORDER BY total_cost DESC
""",
db.conn,
params=(start_date.isoformat(), end_date.isoformat())
)
if not model_stats.empty:
fig = px.pie(
model_stats,
values="total_cost",
names="model",
title="Cost Distribution by Model"
)
st.plotly_chart(fig, use_container_width=True)
st.dataframe(model_stats, use_container_width=True)
# 사용자별 통계 (있는 경우)
st.header("👤 Top Users by Cost")
user_stats = pd.read_sql_query(
"""
SELECT
user_id,
COUNT(*) as requests,
SUM(cost_usd) as total_cost
FROM api_usage
WHERE user_id IS NOT NULL
AND DATE(timestamp) BETWEEN ? AND ?
GROUP BY user_id
ORDER BY total_cost DESC
LIMIT 10
""",
db.conn,
params=(start_date.isoformat(), end_date.isoformat())
)
if not user_stats.empty:
st.dataframe(user_stats, use_container_width=True)
else:
st.info("No user data available")
# 최근 요청
st.header("🕐 Recent Requests")
recent = pd.read_sql_query(
"""
SELECT
timestamp,
user_id,
model,
input_tokens,
output_tokens,
cost_usd
FROM api_usage
ORDER BY timestamp DESC
LIMIT 50
""",
db.conn
)
st.dataframe(recent, use_container_width=True)
실행
# 의존성 설치
pip install streamlit pandas plotly
# 대시보드 실행
streamlit run dashboard.py
# 브라우저에서 http://localhost:8501 열기
Grafana + Prometheus
prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'llm-api'
static_configs:
- targets: ['localhost:8000']
FastAPI 메트릭 엔드포인트
from fastapi import FastAPI
from prometheus_client import Counter, Histogram, generate_latest
app = FastAPI()
# 메트릭 정의
api_requests = Counter(
'llm_api_requests_total',
'Total API requests',
['model', 'status']
)
api_cost = Counter(
'llm_api_cost_usd_total',
'Total cost in USD',
['model']
)
api_tokens = Counter(
'llm_api_tokens_total',
'Total tokens used',
['model', 'type'] # type: input or output
)
api_latency = Histogram(
'llm_api_latency_seconds',
'API request latency',
['model']
)
@app.get("/metrics")
def metrics():
return generate_latest()
@app.post("/chat")
async def chat(request: ChatRequest):
with api_latency.labels(model=request.model).time():
try:
message = client.messages.create(
model=request.model,
max_tokens=1024,
messages=request.messages
)
# 메트릭 업데이트
api_requests.labels(model=request.model, status='success').inc()
cost = calculate_cost(
model=request.model,
input_tokens=message.usage.input_tokens,
output_tokens=message.usage.output_tokens
)
api_cost.labels(model=request.model).inc(cost)
api_tokens.labels(model=request.model, type='input').inc(message.usage.input_tokens)
api_tokens.labels(model=request.model, type='output').inc(message.usage.output_tokens)
return {"response": message.content[0].text}
except Exception as e:
api_requests.labels(model=request.model, status='error').inc()
raise
종합 베스트 프랙티스
모니터링
- 실시간 추적: 모든 API 호출 로깅
- 일일 리포트: 매일 비용 요약 이메일
- 대시보드: 시각화된 통계 확인
- 알림 설정: 임계값 초과 시 즉시 알림
최적화
- 캐싱: 반복 컨텍스트는 Prompt Caching 사용
- 모델 선택: 작업에 맞는 최소 모델 사용
- 컨텍스트 축소: 불필요한 정보 제거
- 배치 처리: 여러 작업을 한 번에 처리
- 로컬 모델: 간단한 작업은 Ollama 활용
예산 관리
- 일일/월간 한도: 명확한 예산 설정
- 사용자별 할당: 팀원별 예산 분배
- 자동 차단: 한도 초과 시 API 호출 중단
- 정기 리뷰: 주간/월간 비용 분석
✅ 비용 관리 체크리스트
- 모든 API 호출을 데이터베이스에 로깅
- 일일 비용 알림 설정 (예: 변동 초과 시)
- 월간 예산 설정 및 모니터링
- Prompt Caching 적용 (반복 컨텍스트)
- 작업별로 적절한 모델 선택 (Haiku/Sonnet/Opus)
- 대시보드 구축 (Streamlit 또는 Grafana)
- 사용자별 할당량 설정
- 주간 비용 리포트 자동화
🚀 다음 단계
- API 키 관리 - 보안을 통한 비용 누수 방지
- CI/CD & LLM - 자동화 파이프라인 비용 최적화
- LLM 개발 환경 - 로컬 개발로 비용 절감
- Ollama 소개 - 무료 로컬 LLM 활용
핵심 정리
- LLM API 비용 모니터링의 핵심 개념과 흐름을 정리합니다.
- LLM 가격 구조를 단계별로 이해합니다.
- 실전 적용 시 기준과 주의점을 확인합니다.
실무 팁
- 입력/출력 예시를 고정해 재현성을 확보하세요.
- LLM API 비용 모니터링 범위를 작게 잡고 단계적으로 확장하세요.
- LLM 가격 구조 조건을 문서화해 대응 시간을 줄이세요.