Skip to content

[Core/Budget] Money 정밀도·통화 불변식과 최종 반올림 교정#50

Closed
Rigu1 wants to merge 13 commits into
tokenpliot:mainfrom
Rigu1:fix/money-invariants-rounding
Closed

[Core/Budget] Money 정밀도·통화 불변식과 최종 반올림 교정#50
Rigu1 wants to merge 13 commits into
tokenpliot:mainfrom
Rigu1:fix/money-invariants-rounding

Conversation

@Rigu1

@Rigu1 Rigu1 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

변경 사항

  • Cost canonical constructor에서 amountcurrencynull을 거부하도록 정리했습니다.
  • usage cost의 음수 금액을 거부하도록 Cost 불변식을 강화했습니다.
  • Cost 생성 시 자동 scale 6, HALF_UP 반올림을 제거해 내부 정밀도를 보존했습니다.
  • Cost.zero()의 암묵적 USD 선택을 제거하고 Cost.zero(currency)로 통화를 명시하도록 변경했습니다.
  • Cost.add(...)Cost.compareTo(...)는 같은 통화에서만 허용하도록 했습니다.
  • scale이 다른 수치상 같은 금액이 동일하게 취급되도록 Cost.equals/hashCode 정책을 정리했습니다.
  • DefaultCostCalculator의 중간 반올림을 제거하고 1K 단가 계산을 rate.multiply(count).movePointLeft(3) 방식으로 변경했습니다.
  • 외부 경계 최종 반올림을 위한 RoundingPolicy.COST_BOUNDARY_ROUNDING을 추가했습니다.
  • Budget의 통화 없는 BigDecimal 금액 경계를 Cost 기반 API로 이관했습니다.
  • BudgetDecision, BudgetEvaluator, BudgetStateStore, DefaultBudgetEvaluator, InMemoryBudgetStateStore가 currency-aware 금액을 다루도록 변경했습니다.
  • Budget 평가 중 limit 통화와 요청 cost 통화가 다르면 누적 상태를 변경하지 않고 CURRENCY_MISMATCH를 반환하도록 했습니다.
  • Cost 정밀도, 통화 불변식, 최종 boundary rounding, Budget currency-aware evaluation에 대한 회귀 테스트를 추가했습니다.

배경

기존 구현은 비용 계산 중간 또는 Cost 생성 시점에 반올림을 적용했습니다. 이로 인해 0.0000004 USD처럼 작은 요청 비용이 0.000000으로 사라질 수 있고, 요청별 반올림 오차가 누적될 수 있었습니다.

또한 Budget API가 BigDecimal만 사용해 금액의 통화를 고려하지 않는 Budget public API에 통화 없는 새 금액 경계가 생겼기 때문에, budget limit과 actual/reservation cost의 통화를 안전하게 비교하기 어려웠습니다.

이번 변경은 Money ADR의 정밀도 정책에 맞춰 내부 금액은 반올림 없이 보존하고, 외부 경계에서만 명시적인 정책으로 최종 반올림하도록 정리합니다.

비용 계산 흐름:

rate × tokenCount × 10^-3
→ 반올림 없이 Cost로 보존
→ Budget admission은 exact amount로 비교
→ 외부 표시/청구 경계에서 RoundingPolicy.COST_BOUNDARY_ROUNDING 1회 적용

수치 예시:

요청 1건 = 0.0000004 USD
1,000건 내부 합계 = 0.0004000 USD
최종 boundary rounding = 0.000400 USD

호환성

  • Cost.value()Cost.amount()로 변경됩니다.
  • Cost.of(value) 대신 Cost.of(value, currency)를 사용해야 합니다.
  • Cost.zero() 대신 Cost.zero(currency)를 사용해야 합니다.
  • Cost는 더 이상 생성자에서 scale 6 반올림을 수행하지 않습니다.
  • Budget API의 금액 타입이 BigDecimal에서 Cost로 변경됩니다.
  • BudgetDecision.currentUsage()BudgetDecision.limit()Cost를 반환합니다.
  • BudgetEvaluator.evaluate(tags, cost)Cost를 받습니다.
  • BudgetStateStore.addCost(tags, cost)Cost를 받습니다.
  • BudgetStateStore.getAccumulatedCost(tags, currency)는 명시 currency를 받아 Cost를 반환합니다.
  • 최종 표시/청구용 반올림은 RoundingPolicy.COST_BOUNDARY_ROUNDING을 명시적으로 적용해야 합니다.

체크리스트

필수 테스트

  • 0.0000004 비용이 Cost 생성 시 0이 되지 않는다.
  • 작은 비용을 여러 번 더한 값이 최종 한 번 반올림한 값과 같다.
  • 1,000 token 단위 일반/cache/reasoning 계산이 정확하다.
  • internal value에는 scale 6 강제가 없다.
  • 외부 boundary에서만 scale 6, HALF_UP이 한 번 적용된다.
  • null amount/currency와 음수 usage cost를 거부한다.
  • zero(currency)가 지정 통화를 보존한다.
  • 서로 다른 통화의 add/compare/evaluate는 상태 변경 없이 실패한다.
  • Core와 Budget public API에 통화 없는 새 금액 경계가 남지 않는다.
  • scale이 다른 수치상 같은 금액의 비교 계약이 일관된다.

Acceptance Criteria

  • Core와 Budget 내부에 요청별 조기 반올림이 없다.
  • 모든 budget/accounting 금액이 currency-aware다.
  • 최종 반올림 담당 API와 boundary가 이름으로 드러난다.
  • :token-ledger-core:test, :token-ledger-budget:test가 통과한다.
  • [ADR] TokenUsage와 Money 불변식 및 정밀도 정책 정의 #24의 Money 정밀도/통화 불변식과 구현이 일치한다.

Closes #22

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • 새로운 기능

    • 비용 모델이 금액과 통화를 함께 관리하도록 개선되었습니다.
    • 통화가 다른 비용을 예산 평가 시 감지하고 별도 상태로 안내합니다.
    • 비용 경계에서 원하는 소수점 자릿수와 반올림 방식을 적용할 수 있습니다.
    • 예산 평가가 사용량·한도·추가 비용을 통화 기준으로 비교합니다.
  • 개선 사항

    • 비용 계산 중 불필요한 중간 반올림을 제거해 정밀도가 향상되었습니다.
    • 비용 입력값 검증이 강화되어 음수 금액과 통화 불일치를 방지합니다.
    • 문서의 비용 계산 및 반올림 기준이 최신 동작에 맞게 업데이트되었습니다.

Walkthrough

Changes

비용 모델이 통화 명시형 Cost로 전환되고, 내부 계산 정밀도 유지 및 외부 경계 반올림 정책이 추가되었습니다. 예산 저장·평가 API도 Cost 기반으로 변경되었으며 통화 불일치 상태와 관련 테스트가 추가되었습니다.

통화 기반 비용 및 예산

Layer / File(s) Summary
Cost 계약과 반올림
token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java, token-ledger-core/src/main/java/io/tokenledger/core/RoundingPolicy.java, token-ledger-core/src/test/...
Cost가 금액과 통화를 필수로 보유하고, 통화 검증·합산·비교·동등성 규칙을 제공합니다. RoundingPolicy는 경계에서 금액을 반올림합니다.
비용 계산과 기본 통화
token-ledger-core/src/main/java/io/tokenledger/core/internal/..., token-ledger-core/src/main/java/io/tokenledger/core/domain/PricingPlan.java, token-ledger-core/src/test/...
토큰 비용 계산이 movePointLeft(3) 방식으로 변경되고, 기본 비용에 명시적 USD 통화가 적용되었습니다.
통화별 예산 저장과 판단
token-ledger-budget/src/main/java/io/tokenledger/budget/..., token-ledger-budget/src/test/...
예산 결정·평가·저장이 CostCurrency를 사용하며, 통화 불일치 시 CURRENCY_MISMATCH를 반환하고 Cost 기반으로 허용·경고·차단을 판단합니다.

Suggested reviewers: huitaepark, huitaepark

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning 직접 링크된 #22는 TokenUsage 합산/불변식 요구인데, 이번 변경은 주로 money와 budget 처리라 요구사항을 충족하지 않습니다. #22를 대상으로 하려면 TokenUsage 불변식, cache/reasoning 집계, overflow 검출, 중복 과금 방지 로직이 코드에 포함되어야 합니다.
Out of Scope Changes check ⚠️ Warning TokenUsage 중심의 #22 범위와 무관한 money precision, currency-aware budget, rounding 정책 변경이 다수 포함되어 있습니다. Linked issue 범위에 맞게 TokenUsage 관련 변경만 남기고, money/budget 정밀도 작업은 별도 이슈로 분리하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 핵심 변경인 Money 정밀도, 통화 불변식, 최종 반올림 교정을 잘 요약합니다.
Description check ✅ Passed 설명이 변경된 Cost, Budget API, RoundingPolicy, 회귀 테스트 내용을 전반적으로 잘 반영합니다.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/money-invariants-rounding

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from HuitaePark July 23, 2026 11:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetEvaluator.java (1)

20-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Javadoc에 CURRENCY_MISMATCH 반환 케이스가 누락됨

evaluate(tags, cost)가 이제 BudgetState.CURRENCY_MISMATCH를 반환할 수 있는데, 인터페이스 문서는 여전히 "예산 초과 여부만 판단합니다(부수 효과 없음)"로만 설명되어 있어 호출자가 이 상태를 놓칠 위험이 있습니다.

♻️ 제안
     /**
-     * 이번 호출로 발생할 예상 비용을 포함하여 예산 초과 여부를 판단합니다. (부수 효과 없음)
+     * 이번 호출로 발생할 예상 비용을 포함하여 예산 초과 여부를 판단합니다. (부수 효과 없음)
+     * <p>
+     * {`@code` cost}의 통화가 예산 한도 통화와 다르면 {`@link` BudgetState#CURRENCY_MISMATCH} 상태를 반환합니다.
      */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetEvaluator.java`
around lines 20 - 26, Update the Javadoc for the BudgetEvaluator.evaluate method
to document that it can return BudgetState.CURRENCY_MISMATCH in addition to
budget-exceeded outcomes. Preserve the existing side-effect-free behavior
description and make the currency mismatch case explicit for callers.
token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java (1)

65-79: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

BLOCK 테스트가 예외 타입만 검증, Cost 값은 검증하지 않음

다른 테스트들은 decision.currentUsage()/decision.limit()Cost로 검증하는데, 이 테스트는 BudgetExceededException 타입만 확인합니다. BudgetExceededException이 내부 BudgetDecision을 노출한다면 nextUsage/monthlyLimit 값도 함께 검증하는 것이 이번 PR의 통화/정밀도 회귀 테스트 취지에 더 부합합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java`
around lines 65 - 79, The shouldThrowExceptionWhenUsageExceedsLimit test only
checks the exception type; extend it to inspect the BudgetExceededException’s
exposed BudgetDecision and assert its nextUsage and monthlyLimit Cost values
match usd("120") and usd("100"), respectively. Preserve the existing exception
assertion and inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@token-ledger-budget/src/main/java/io/tokenledger/budget/internal/DefaultBudgetEvaluator.java`:
- Around line 43-51: Update the currency validation used by
DefaultBudgetEvaluator to validate accumulated as well as cost against
monthlyLimit before calling accumulated.add(cost). Apply the same validation to
the secondary evaluation path around the referenced lines, and route any
mismatch through currencyMismatchDecision rather than allowing Cost.add to
throw. Reuse or extend hasCurrencyMismatch so both store-derived and incoming
costs are covered.
- Around line 10-15: DefaultBudgetEvaluator 클래스의 Javadoc에서 “판단 기준” 목록을 올바른 항목별
형식으로 정리하고, currency 불일치 시 CURRENCY_MISMATCH가 발생한다는 기준을 명시하세요. 목록과 이어 붙은 순수 함수
설명을 분리해 Javadoc 문장이 자연스럽게 렌더링되도록 수정하세요.

In
`@token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java`:
- Around line 28-30: Update InMemoryBudgetStateStore#getAccumulatedCost and
addCost so the state key includes both the tenant tags and currency, using the
same key construction for reads and writes. Ensure requests for different
currencies cannot share or return the same accumulated Cost.

In `@token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java`:
- Around line 11-12: Update the public Javadoc for the Cost constructor
parameters to remove the claim that currency defaults to USD and that amount is
limited or rounded to six decimal places. Document only the current behavior and
precision contract for amount and currency.

In `@token-ledger-core/src/test/java/io/tokenledger/core/domain/CostTest.java`:
- Around line 48-54: Update shouldRejectAddWithNullCost to invoke Cost.add(null)
instead of Cost.compareTo(null), so the test validates Cost.add’s null rejection
contract. Keep the existing Cost setup and NullPointerException assertion
unchanged.

In
`@token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultLedgerManagerTest.java`:
- Line 56: 기본 요금제 테스트에서 result.amount()만 검증하지 말고 결과의 통화 필드/접근자도 함께 검증하도록 수정하세요.
누락 요금제의 fallback 통화가 USD임을 명시적으로 확인하고, 기존 금액 검증은 유지하세요.

---

Nitpick comments:
In
`@token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetEvaluator.java`:
- Around line 20-26: Update the Javadoc for the BudgetEvaluator.evaluate method
to document that it can return BudgetState.CURRENCY_MISMATCH in addition to
budget-exceeded outcomes. Preserve the existing side-effect-free behavior
description and make the currency mismatch case explicit for callers.

In
`@token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java`:
- Around line 65-79: The shouldThrowExceptionWhenUsageExceedsLimit test only
checks the exception type; extend it to inspect the BudgetExceededException’s
exposed BudgetDecision and assert its nextUsage and monthlyLimit Cost values
match usd("120") and usd("100"), respectively. Preserve the existing exception
assertion and inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: acdb0dda-fa76-43c5-be8f-3f329452ebe1

📥 Commits

Reviewing files that changed from the base of the PR and between dc8a162 and 6713330.

📒 Files selected for processing (17)
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetDecision.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetEvaluator.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetState.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetStateStore.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/DefaultBudgetEvaluator.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/LedgerBudgetComponents.java
  • token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java
  • token-ledger-core/src/main/java/io/tokenledger/core/RoundingPolicy.java
  • token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java
  • token-ledger-core/src/main/java/io/tokenledger/core/domain/PricingPlan.java
  • token-ledger-core/src/main/java/io/tokenledger/core/internal/DefaultCostCalculator.java
  • token-ledger-core/src/main/java/io/tokenledger/core/internal/DefaultLedgerManager.java
  • token-ledger-core/src/test/java/io/tokenledger/core/RoundingPolicyTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/domain/CostTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultCostCalculatorTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultLedgerManagerTest.java

Comment on lines +28 to +30
public Cost getAccumulatedCost(Map<String, String> tags, Currency currency) {
return store.getOrDefault(key(tags), Cost.zero(currency));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate relevant files
git ls-files | rg 'token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java|token-ledger-budget/src/main/java/io/tokenledger/budget|Cost.java|DefaultBudgetEvaluator.java'

# Map the target file structure
ast-grep outline token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java --view expanded

# Inspect the target file with line numbers
wc -l token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java
cat -n token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java

# Find the Cost implementation and evaluator
rg -n "class Cost|record Cost|interface Cost|currency\(\)|add\(|CURRENCY_MISMATCH|evaluate\(" token-ledger-budget/src/main/java -S

# Print the relevant Cost and evaluator files if found
for f in $(git ls-files 'token-ledger-budget/src/main/java/**' | rg 'Cost\.java|DefaultBudgetEvaluator\.java'); do
  echo "===== $f ====="
  wc -l "$f"
  cat -n "$f"
done

Repository: tokenpliot/tokenpilot

Length of output: 8069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Cost and store contract
wc -l token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java \
      token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetStateStore.java

cat -n token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java
printf '\n===== BudgetStateStore =====\n'
cat -n token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetStateStore.java

# Check whether addCost/getAccumulatedCost are used elsewhere in a way that depends on currency matching
rg -n "addCost\\(|getAccumulatedCost\\(" token-ledger-budget token-ledger-core -S

Repository: tokenpliot/tokenpilot

Length of output: 5166


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect relevant tests and usages around currency mismatch
rg -n "CURRENCY_MISMATCH|currency mismatch|addCost\\(|getAccumulatedCost\\(" token-ledger-budget/src/test token-ledger-budget/src/main -S

# Show the DefaultBudgetEvaluator tests around the store interaction
wc -l token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java
cat -n token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java

# Probe JDK merge/remapping exception behavior with a tiny standalone program
cat >/tmp/merge_probe.java <<'EOF'
import java.util.concurrent.ConcurrentHashMap;
import java.util.*;

public class merge_probe {
  public static void main(String[] args) {
    var map = new ConcurrentHashMap<String, String>();
    map.put("k", "USD");
    try {
      map.merge("k", "EUR", (a,b) -> { throw new IllegalArgumentException("boom"); });
      System.out.println("no-throw");
    } catch (Exception e) {
      System.out.println(e.getClass().getName() + ":" + e.getMessage());
    }
    System.out.println(map.get("k"));
  }
}
EOF
javac /tmp/merge_probe.java && java -cp /tmp merge_probe

Repository: tokenpliot/tokenpilot

Length of output: 7744


InMemoryBudgetStateStore#getAccumulatedCost/addCost(28-35)에서 통화도 상태 키에 포함하세요

key(tags)tenant_id만 써서 같은 태그에 서로 다른 통화가 섞일 수 있습니다.

  • getAccumulatedCost(tags, currency)currency를 무시해 요청한 통화와 다른 Cost를 그대로 반환할 수 있습니다.
  • 그 상태에서 DefaultBudgetEvaluator.evaluate()accumulated.add(cost)Cost.add()의 통화 검증에서 예외를 던질 수 있습니다.
  • addCost()만 검증해도 읽기 경로가 남으니, 통화를 키에 포함하거나 조회/추가 모두에서 동일하게 차단하세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java`
around lines 28 - 30, Update InMemoryBudgetStateStore#getAccumulatedCost and
addCost so the state key includes both the tenant tags and currency, using the
same key construction for reads and writes. Ensure requests for different
currencies cannot share or return the same accumulated Cost.

Comment thread token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java Outdated
Cost result = manager.record("unknown-model", usage, Map.of());

assertThat(result.value()).isEqualByComparingTo(BigDecimal.ZERO);
assertThat(result.amount()).isEqualByComparingTo(BigDecimal.ZERO);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

누락 요금제의 기본 통화도 검증하세요.

현재는 금액만 검증하므로 USD fallback이 다른 통화로 바뀌어도 테스트가 통과합니다. 명시적 통화는 이번 변경의 계약이므로 함께 확인해야 합니다.

수정 예시
+import java.util.Currency;
+
 assertThat(result.amount()).isEqualByComparingTo(BigDecimal.ZERO);
+assertThat(result.currency()).isEqualTo(Currency.getInstance("USD"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assertThat(result.amount()).isEqualByComparingTo(BigDecimal.ZERO);
assertThat(result.amount()).isEqualByComparingTo(BigDecimal.ZERO);
assertThat(result.currency()).isEqualTo(Currency.getInstance("USD"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultLedgerManagerTest.java`
at line 56, 기본 요금제 테스트에서 result.amount()만 검증하지 말고 결과의 통화 필드/접근자도 함께 검증하도록 수정하세요.
누락 요금제의 fallback 통화가 USD임을 명시적으로 확인하고, 기존 금액 검증은 유지하세요.

@Rigu1
Rigu1 force-pushed the fix/money-invariants-rounding branch from 6713330 to 59dc355 Compare July 23, 2026 12:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java (1)

22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

기본 통화 상수를 불변으로 만들고 모든 기본 생성 경로에서 재사용하세요.

현재 DEFAULT_CURRENCY는 변경 가능한 package-private 필드이며, Line 48의 3-인자 생성자는 여전히 "USD"를 직접 사용합니다. 한 경로의 기본 통화만 변경되면 생성 방식에 따라 결과가 달라질 수 있습니다.

수정 예시
-    static String DEFAULT_CURRENCY = "USD";
+    private static final String DEFAULT_CURRENCY = "USD";

-        this(modelId, promptPricePerK, completionPricePerK, Currency.getInstance("USD"));
+        this(modelId, promptPricePerK, completionPricePerK, Currency.getInstance(DEFAULT_CURRENCY));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java` at
line 22, PricingPlan의 DEFAULT_CURRENCY를 static final 상수로 변경하고, 3-인자 생성자를 포함한 모든
기본 생성 경로에서 직접 지정한 "USD" 대신 DEFAULT_CURRENCY를 재사용하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java`:
- Around line 24-25: Update the PricingPlan constructor to handle an empty rates
map safely, since PricingPlanProperties may provide Map.of() when rates are
omitted. Initialize the EnumMap with TokenType.class before copying rates, or
explicitly reject empty input if that matches the intended contract, while
preserving the unmodifiable-map behavior.

---

Nitpick comments:
In `@token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java`:
- Line 22: PricingPlan의 DEFAULT_CURRENCY를 static final 상수로 변경하고, 3-인자 생성자를 포함한
모든 기본 생성 경로에서 직접 지정한 "USD" 대신 DEFAULT_CURRENCY를 재사용하세요.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ee4e023-a218-4c43-9822-291ac508e7b4

📥 Commits

Reviewing files that changed from the base of the PR and between 6713330 and 59dc355.

📒 Files selected for processing (18)
  • README.md
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetEvaluator.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetState.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStore.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/LedgerBudgetComponents.java
  • token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluatorTest.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/RoundingPolicy.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/RoundingPolicyTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/CostTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultCostCalculatorTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultLedgerManagerTest.java

Comment on lines 24 to 25
public PricingPlan {
rates = Collections.unmodifiableMap(new EnumMap<>(rates));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

jshell <<'EOF'
import java.util.*;

Map<Thread.State, Integer> empty = Map.of();
try {
    new EnumMap<Thread.State, Integer>(empty);
    throw new AssertionError("IllegalArgumentException이 예상됩니다");
} catch (IllegalArgumentException expected) {
    System.out.println("verified");
}
EOF

Repository: tokenpliot/tokenpilot

Length of output: 203


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

git ls-files | rg 'token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java|token-pilot-core/src/test|PricingPlan'

Repository: tokenpliot/tokenpilot

Length of output: 981


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '1,200p' token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java

Repository: tokenpliot/tokenpilot

Length of output: 2953


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java')
print(p.exists(), p)
PY

Repository: tokenpliot/tokenpilot

Length of output: 238


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '1,220p' token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java

Repository: tokenpliot/tokenpilot

Length of output: 2953


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n== PricingPlan usages ==\n'
rg -n --no-heading "new PricingPlan\\(" token-pilot-core token-pilot-autoconfigure || true

printf '\n== Prices properties / defaults ==\n'
sed -n '1,220p' token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/PricingPlanProperties.java

printf '\n== EnumMap empty-map probe ==\n'
cat >/tmp/EnumMapEmptyProbe.java <<'EOF'
import java.util.*;

public class EnumMapEmptyProbe {
  enum K { A }
  public static void main(String[] args) {
    Map<K, Integer> empty = new HashMap<>();
    try {
      new EnumMap<K, Integer>(empty);
      System.out.println("NO_EXCEPTION");
    } catch (Throwable t) {
      System.out.println(t.getClass().getName());
      System.out.println(t.getMessage());
    }
  }
}
EOF
javac /tmp/EnumMapEmptyProbe.java && java -cp /tmp EnumMapEmptyProbe

Repository: tokenpliot/tokenpilot

Length of output: 2551


rates 입력을 처리하세요 PricingPlanProperties가 기본값으로 Map.of()를 넘기므로, 설정에서 rates를 생략하면 new EnumMap<>(rates)에서 예외가 납니다. TokenType.class로 초기화한 뒤 putAll 하거나 빈 맵을 명시적으로 거부하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java`
around lines 24 - 25, Update the PricingPlan constructor to handle an empty
rates map safely, since PricingPlanProperties may provide Map.of() when rates
are omitted. Initialize the EnumMap with TokenType.class before copying rates,
or explicitly reject empty input if that matches the intended contract, while
preserving the unmodifiable-map behavior.

@Rigu1
Rigu1 force-pushed the fix/money-invariants-rounding branch from 59dc355 to 5cd6d85 Compare July 23, 2026 12:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultCostCalculatorTest.java`:
- Around line 81-93: Update the `@DisplayName` and
shouldUseRateAsCostForOneThousandTokens test method name to describe a one-token
calculation, matching TokenUsage.from(1, 0) and the asserted cost while
preserving the test logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebe216f5-161a-42f4-a95c-eea45eb40bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 59dc355 and 5cd6d85.

📒 Files selected for processing (18)
  • README.md
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetDecision.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetEvaluator.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetState.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/BudgetStateStore.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/DefaultBudgetEvaluator.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/InMemoryBudgetStateStore.java
  • token-ledger-budget/src/main/java/io/tokenledger/budget/internal/LedgerBudgetComponents.java
  • token-ledger-budget/src/test/java/io/tokenledger/budget/internal/DefaultBudgetEvaluatorTest.java
  • token-ledger-core/src/main/java/io/tokenledger/core/RoundingPolicy.java
  • token-ledger-core/src/main/java/io/tokenledger/core/domain/Cost.java
  • token-ledger-core/src/main/java/io/tokenledger/core/domain/PricingPlan.java
  • token-ledger-core/src/main/java/io/tokenledger/core/internal/DefaultCostCalculator.java
  • token-ledger-core/src/main/java/io/tokenledger/core/internal/DefaultLedgerManager.java
  • token-ledger-core/src/test/java/io/tokenledger/core/RoundingPolicyTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/domain/CostTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultCostCalculatorTest.java
  • token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultLedgerManagerTest.java

Comment on lines +81 to +93
@Test
@DisplayName("1,000 token은 1K 단가 그대로 계산되어야 한다")
void shouldUseRateAsCostForOneThousandTokens() {
PricingPlan plan = new PricingPlan(
"tiny-model",
new BigDecimal("0.0004"),
BigDecimal.ZERO
);
TokenUsage usage = TokenUsage.from(1, 0);

Cost cost = calculator.calculate(usage, plan);

assertThat(cost.amount()).isEqualByComparingTo("0.0000004");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

테스트 이름을 실제 입력과 맞추세요.

Line 89는 1 토큰을 계산하지만 표시명과 메서드명은 1,000 토큰을 검증한다고 설명합니다. 미세 비용 보존 회귀 테스트가 목적이라면 이름을 1 토큰 기준으로 바꾸세요.

수정 예시
-    `@DisplayName`("1,000 token은 1K 단가 그대로 계산되어야 한다")
-    void shouldUseRateAsCostForOneThousandTokens() {
+    `@DisplayName`("1 token 비용은 1K 단가의 1/1000으로 계산되어야 한다")
+    void shouldCalculateFractionalRateForSingleToken() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
@DisplayName("1,000 token은 1K 단가 그대로 계산되어야 한다")
void shouldUseRateAsCostForOneThousandTokens() {
PricingPlan plan = new PricingPlan(
"tiny-model",
new BigDecimal("0.0004"),
BigDecimal.ZERO
);
TokenUsage usage = TokenUsage.from(1, 0);
Cost cost = calculator.calculate(usage, plan);
assertThat(cost.amount()).isEqualByComparingTo("0.0000004");
`@Test`
`@DisplayName`("1 token 비용은 1K 단가의 1/1000으로 계산되어야 한다")
void shouldCalculateFractionalRateForSingleToken() {
PricingPlan plan = new PricingPlan(
"tiny-model",
new BigDecimal("0.0004"),
BigDecimal.ZERO
);
TokenUsage usage = TokenUsage.from(1, 0);
Cost cost = calculator.calculate(usage, plan);
assertThat(cost.amount()).isEqualByComparingTo("0.0000004");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@token-ledger-core/src/test/java/io/tokenledger/core/internal/DefaultCostCalculatorTest.java`
around lines 81 - 93, Update the `@DisplayName` and
shouldUseRateAsCostForOneThousandTokens test method name to describe a one-token
calculation, matching TokenUsage.from(1, 0) and the asserted cost while
preserving the test logic.

@Rigu1 Rigu1 closed this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Core] TokenUsage 불변식과 토큰 합산 의미 교정

1 participant