AI 호출 비용을 토큰 단위로 기록하고 모델별로 계산하기
크레딧 가격이 실제 AI 비용과 맞는지 알려면 호출마다 무엇을 얼마나 썼는지 남겨야 해요. DropThePitch가 AI 사용량을 기록하는 방법과, 모델마다 다른 요율로 비용을 계산하는 방법을 정리했어요.
DropThePitch는 프로젝트 하나에 AI를 서른 번 넘게 불러요. 크레딧 가격을 정할 때 가장 먼저 궁금했던 것은 "실제로 얼마가 드는가"였어요. 감으로 정한 가격은 손해를 보거나, 사용자에게 너무 비싸거나 둘 중 하나예요. 그래서 처음부터 모든 AI 호출을 기록하기로 했어요.
호출마다 남기는 것
기록은 호출 하나에 한 줄이에요.
@Entity
@Table(name = "ai_usage")
public class AiUsage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "project_id", nullable = false)
private Project project;
@Column(nullable = false)
private String model;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private AiPurpose purpose;
@Column(nullable = false)
private Integer inputTokens;
@Column(nullable = false)
private Integer outputTokens;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime requestedAt;
}
각 값을 남긴 이유가 있어요.
- 프로젝트: 프로젝트 하나에 평균 얼마가 드는지 알아야 크레딧 가격을 정할 수 있어요.
- 모델: 모델마다 요율이 달라요. 나중에 모델을 바꿔도 예전 기록의 비용을 정확히 계산하려면 그때 쓴 모델을 알아야 해요.
- 용도: 파일 분석, 페르소나 선별, 의견 수집, 리포트 생성 중 어디에 비용이 몰리는지 알아야 줄일 곳을 찾을 수 있어요.
- 입력, 출력 토큰: 입력과 출력은 요율이 달라서 따로 남겨요.
비용 자체는 저장하지 않았어요. 토큰 수와 모델만 있으면 언제든 계산할 수 있고, 요율이 바뀌거나 잘못 적은 요율을 고쳐도 기록을 다시 쓸 필요가 없어요.
모델 이름은 응답에서 읽어요
모델 이름은 설정 파일에 적은 값이 아니라, AI가 응답과 함께 돌려준 값을 써요.
Usage usage = chatResponse.getMetadata().getUsage();
return new TagSelectionCallResult(
result,
chatResponse.getMetadata().getModel(),
tokenCount(usage == null ? null : usage.getPromptTokens()),
tokenCount(usage == null ? null : usage.getCompletionTokens()));
설정을 바꾸고 서버를 다시 띄우는 사이에 이미 진행 중이던 호출이 있을 수 있어요. 응답에 적힌 모델이 실제로 답한 모델이에요.
토큰 수는 응답에 없을 수도 있어요. 그때는 0으로 기록하고 경고 로그를 남겨요. 사용량 기록을 못 한다고 사용자의 분석을 실패시킬 수는 없어요.
private int tokenCount(Integer value) {
if (value == null) {
log.warn("[selectTags] 토큰 사용량이 응답에 없어 0으로 기록합니다.");
return 0;
}
return value;
}
의견 수집처럼 호출이 많은 곳에서는 기록 저장 자체도 실패해도 넘어가게 했어요. 의견은 이미 받았는데 기록 때문에 그 의견을 버리면 안 되니까요.
요율표
비용은 100만 토큰당 달러로 매겨져요. 모델별 요율을 표로 두었어요.
private static final Map<String, Rate> RATES = Map.ofEntries(
Map.entry("gemini-3.5-flash", new Rate(1.50, 9.00)),
Map.entry("gemini-3.5-flash-lite", new Rate(0.30, 2.50)),
Map.entry("gemini-3.1-flash-lite", new Rate(0.25, 1.50)),
Map.entry("gemini-2.5-pro", new Rate(1.25, 10.00)),
Map.entry("gemini-2.5-flash", new Rate(0.30, 2.50)),
Map.entry("gemini-2.5-flash-lite", new Rate(0.10, 0.40))
);
public static double costOf(String model, long inputTokens, long outputTokens) {
Rate rate = rateOf(model);
return inputTokens / TOKENS_PER_UNIT * rate.input() + outputTokens / TOKENS_PER_UNIT * rate.output();
}
표를 보면 출력이 입력보다 몇 배씩 비싸요. 같은 토큰 수라면 답을 길게 받는 쪽이 비용에 더 크게 반영돼요. 그래서 기록에서도 입력과 출력을 꼭 나눠서 봐요.
표에 없는 모델이 오면 기본 모델의 요율로 계산해요. 비용이 0으로 나오는 것보다는 대략이라도 잡히는 편이 나아요.
합산할 때는 모델별로 나눠서
관리자 화면에서는 날짜별, 용도별, 프로젝트별 비용을 봐요. 처음에는 토큰을 먼저 다 더한 뒤 한 번에 비용을 계산하려고 했어요. 하지만 그러면 틀려요. 비싼 모델의 토큰 100만 개와 싼 모델의 토큰 100만 개를 더해 버리면, 어느 요율을 곱해야 할지 알 수 없어요.
그래서 쿼리에서 묶을 때 항상 모델을 함께 묶어요.
+ " group by " + keyExpression + ", a.model";
날짜별로 볼 때는 "날짜와 모델"로, 용도별로 볼 때는 "용도와 모델"로 묶어요. 묶인 줄마다 그 모델의 요율로 비용을 계산한 다음, 같은 날짜나 같은 용도끼리 비용을 더해요.
public AiUsageSum add(String model, long calls, long inputTokens, long outputTokens) {
this.calls += calls;
this.inputTokens += inputTokens;
this.outputTokens += outputTokens;
this.totalCost += GeminiPricing.costOf(model, inputTokens, outputTokens);
return this;
}
토큰은 모델과 상관없이 더해도 되지만, 비용은 모델별로 계산한 뒤에 더해야 해요.
관리자 화면에서 보는 것
이 기록 위에 관리자 화면을 만들었어요. 볼 수 있는 것은 이래요.
- 전체 호출 수, 토큰 수, 비용 합계와 호출 하나당 평균 비용
- 날짜별 비용 그래프
- 파일 분석, 페르소나 선별, 의견 수집, 리포트 생성의 용도별 비용
- 모델별 사용량과 요율
- 프로젝트별 비용과 최근 호출 목록
날짜별 그래프는 기록이 없는 날도 0으로 채워서 돌려줘요. 빈 날을 빼 버리면 그래프의 간격이 어긋나서 사용량이 꾸준한 것처럼 보이기 때문이에요. 기간이 너무 길어지지 않도록 최대 366일까지만 보여줘요.
크레딧 가격이 실제 비용을 넘는지, 어느 용도에 비용이 몰리는지는 이 화면에서 기간을 정해 놓고 확인해요.
프로젝트를 지워도 이 기록은 남겨요. 프로젝트는 지운 시각만 적어 두고 행을 그대로 두기 때문에, 기록이 어느 프로젝트에서 나왔는지도 계속 알 수 있어요.
정리
- AI 호출마다 프로젝트, 모델, 용도, 입력과 출력 토큰을 남겨요.
- 비용은 저장하지 않고 토큰과 요율표로 그때그때 계산해요. 요율이 바뀌어도 기록을 고칠 필요가 없어요.
- 모델 이름은 설정이 아니라 응답에서 읽어요.
- 합산할 때는 항상 모델별로 묶어서 비용을 계산한 뒤에 더해요.
- 사용량 기록이 실패해도 사용자의 결과는 버리지 않아요.