Skip to content

[Budget] 월별 budget window를 Clock 기반으로 교정#49

Merged
HuitaePark merged 1 commit into
mainfrom
fix/issue-27-budget-window
Jul 24, 2026
Merged

[Budget] 월별 budget window를 Clock 기반으로 교정#49
HuitaePark merged 1 commit into
mainfrom
fix/issue-27-budget-window

Conversation

@Leejaewang03

Copy link
Copy Markdown
Contributor

변경 내용

  • policy/target/window를 보존하는 불변 BudgetKeyBudgetWindow를 추가했습니다.
  • 주입된 Clock과 설정된 ZoneId 기준으로 월별 window를 결정합니다.
  • target identity 누락은 fail-closed로 처리하고, 명시적으로 설정된 fallback만 허용합니다.
  • 동일 key의 limit/currency snapshot을 최초 조회에서 고정하고 currency mismatch 시 상태를 변경하지 않습니다.
  • evaluator가 확정한 key를 store, Spring AI advisor, notification에 그대로 전달합니다.

@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.

@HuitaePark
HuitaePark self-requested a review July 22, 2026 05:24
@HuitaePark

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • 새로운 기능
    • 예산 정책에 정책 ID, 대상 유형·식별자, 통화 및 시간대를 설정할 수 있습니다.
    • 월별 예산 기간이 지정된 시간대와 기준 시계에 따라 정확히 계산됩니다.
    • 예산 상태와 비용이 정책·대상·기간별로 명확하게 관리됩니다.
  • 개선 사항
    • 예산 초과 및 임계치 알림의 중복 발송을 더욱 정확하게 방지합니다.
    • 비용 누적 시 통화와 예산 한도 불일치를 검증합니다.
    • 예산 초과 메시지의 사용량 표시 형식을 개선했습니다.

Walkthrough

예산 정책과 월별 BudgetKey·BudgetWindow를 도입하고, 평가·비용 저장·자동 구성·Spring AI 비용 전파·알림 중복 방지 흐름을 해당 키와 통화·시간대 정보 기반으로 변경했습니다.

Changes

예산 식별자 기반 흐름

Layer / File(s) Summary
예산 정책 및 데이터 계약
token-pilot-budget/src/main/java/io/tokenpilot/budget/*
BudgetPolicy, BudgetKey, BudgetWindow를 추가하고 BudgetDecisionBudgetStateStore 계약에 키와 통화·정책 스냅샷 정보를 반영했습니다.
정책 기반 평가 및 비용 저장
token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/*, token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/*
ClockBudgetPolicy로 월별 키를 생성하고, 키별 누적 비용과 limit/currency 스냅샷을 검증하도록 평가기·인메모리 저장소와 테스트를 변경했습니다.
자동 구성 및 시계 주입
token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/*, token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/*
설정 속성을 BudgetPolicy로 변환하고, 주입된 Clock 또는 UTC 기본 시계를 BudgetEvaluator에 전달하도록 자동 구성을 변경했습니다.
Spring AI 비용 전파
token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/*, token-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/*, token-pilot-sample-app/src/main/java/io/tokenpilot/sample/SampleController.java
요청 컨텍스트에 BudgetDecision을 저장한 뒤 응답 후 해당 키·한도·통화로 비용을 누적하도록 변경하고 샘플 호출부를 새 저장소 계약에 맞췄습니다.
BudgetKey 기반 알림 상태
token-pilot-notification/src/main/java/io/tokenpilot/notification/*, token-pilot-notification/src/test/java/io/tokenpilot/notification/*
알림 이벤트와 마지막 임계치 상태 조회·갱신을 BudgetKey 기반으로 전환하고 관련 테스트를 갱신했습니다.
설정 및 변경 이력 문서
AGENTS.md
BudgetKey 기반 중복 방지와 정책·대상·통화·시간대 설정 예시 및 변경 이력을 갱신했습니다.

Suggested reviewers: huitaepark

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Clock 기반 월별 budget window 교정이라는 핵심 변경을 정확하고 간결하게 요약합니다.
Description check ✅ Passed 설명이 BudgetKey, BudgetWindow, Clock/ZoneId, fallback, store 전달까지 변경 내용을 전반적으로 잘 반영합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java (1)

37-60: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

동일 key의 알림 선점을 원자화하세요.

동시 요청 두 건이 모두 이전 threshold를 읽고 각각 이벤트를 발행한 뒤 상태를 갱신할 수 있어, 동일 BudgetKey·threshold의 중복 알림이 발생합니다. 조회와 갱신을 분리한 현재 계약으로는 이를 막을 수 없습니다.

  • token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java#L37-L60: 별도 조회·갱신 대신 원자적 claim 결과가 성공한 경우에만 이벤트를 발행하세요.
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java#L11-L16: 예: advanceIfHigher(BudgetKey, BudgetThreshold): boolean 형태의 원자적 상태 전이 계약으로 교체하세요.
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java#L16-L27: ConcurrentHashMap.compute 등으로 threshold 비교와 갱신을 하나의 원자적 연산으로 구현하세요.
  • token-pilot-notification/src/test/java/io/tokenpilot/notification/BudgetNotificationServiceTest.java#L25-L52: 같은 key·threshold를 동시에 호출해 handler가 한 번만 실행되는 회귀 테스트를 추가하세요.

As per coding guidelines, 알림 중복 제거는 evaluator-resolved BudgetKey를 사용하고 동일 window의 동등하거나 낮은 threshold를 억제해야 합니다.

🤖 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-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java`
around lines 37 - 60, 동일 key·threshold 알림의 중복 발행을 막도록 원자적 claim 흐름으로 변경하세요.
token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java:37-60에서는
evaluator-resolved BudgetKey로 NotificationStateStore.advanceIfHigher(...)를 호출하고
성공한 경우에만 이벤트를 발행하세요.
token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java:11-16의
계약을 해당 boolean 원자적 상태 전이로 교체하고,
token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java:16-27에서는
ConcurrentHashMap.compute 등으로 동일 window의 동등하거나 낮은 threshold를 억제하며 비교·갱신을 원자화하세요.
token-pilot-notification/src/test/java/io/tokenpilot/notification/BudgetNotificationServiceTest.java:25-52에는
같은 key·threshold의 동시 호출 시 handler가 한 번만 실행되는 회귀 테스트를 추가하세요.

Source: Coding guidelines

token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java (2)

63-88: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

after()의 방어 조건 불일치 및 통화 불일치 시 전체 AI 응답이 깨질 수 있습니다.

두 가지 결함이 결합되어 있습니다.

  1. 가드 조건이 budgetStateStore/costCalculator/pricingRegistry != null만 확인하고 budgetEvaluator != null은 확인하지 않습니다. budgetEvaluator가 없으면 before()에서 컨텍스트에 decision이 채워지지 않는데, after()는 여전히 예산 블록을 실행해 extractBudgetDecisionIllegalStateException을 던지게 됩니다.
  2. costCalculator.calculate(...)는 pricing plan의 currency로 Cost를 만드는데, decision.currency()는 budget policy의 currency입니다. 두 currency가 다르면(멀티-currency 가격 정책 구성 시 충분히 발생 가능) addCostIllegalArgumentException을 던집니다.

두 경우 모두 after()에서 잡히지 않은 예외가 advisor 체인 밖으로 전파되어, 실제 LLM 응답 자체가 실패하게 됩니다(장부 기록 ledgerManager.record는 이미 끝난 뒤이므로 응답만 깨지는 것이 더 안타까운 지점). 최소한 이 블록을 방어적으로 처리(예: catch 후 로깅)해 예산/가격 설정 문제가 사용자 응답을 막지 않도록 하는 것을 권장합니다.

🛡️ 방어적 처리 제안
         if (budgetStateStore != null && costCalculator != null && pricingRegistry != null) {
             Optional<PricingPlan> plan = pricingRegistry.getPlan(modelId);
             if (plan.isPresent()) {
                 Cost cost = costCalculator.calculate(usage, plan.get());
-                BudgetDecision decision = extractBudgetDecision(response);
-                budgetStateStore.addCost(
-                    decision.key(),
-                    decision.limit(),
-                    decision.currency(),
-                    cost
-                );
+                try {
+                    BudgetDecision decision = extractBudgetDecision(response);
+                    budgetStateStore.addCost(
+                        decision.key(),
+                        decision.limit(),
+                        decision.currency(),
+                        cost
+                    );
+                } catch (IllegalStateException | IllegalArgumentException e) {
+                    // 예산 설정/통화 불일치가 실제 AI 응답을 막지 않도록 함
+                    log.warn("Skipping budget cost accumulation: {}", e.getMessage());
+                }
             }
         }
🤖 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-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`
around lines 63 - 88, Update the budget-processing block in
DefaultLedgerAdvisor.after to also require budgetEvaluator before calling
extractBudgetDecision, and defensively catch failures from decision extraction,
cost calculation, or budgetStateStore.addCost so pricing or budget configuration
errors are logged without preventing the original ChatClientResponse from
returning. Preserve ledgerManager.record and always return response.

51-61: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

before()에서 BLOCK을 차단해야 합니다. budgetEvaluator.evaluate(tags)의 결과를 context에만 넣고 request를 그대로 반환해서, BudgetState.BLOCK이어도 provider 호출이 계속됩니다. 예산 초과 시에는 여기서 예외를 던지거나 요청을 중단해야 합니다.

🤖 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-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`
around lines 51 - 61, DefaultLedgerAdvisor의 before()에서
budgetEvaluator.evaluate(tags) 결과가 BudgetState.BLOCK이면 provider 호출로 진행되지 않도록 요청을
중단하거나 예외를 던지도록 수정하세요. 차단되지 않은 결정만 기존처럼 BUDGET_DECISION_CONTEXT에 저장한 변형된 요청을
반환하고, evaluator가 없는 경우의 기존 동작은 유지하세요.
🤖 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-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java`:
- Around line 8-15: Redefine the BudgetStateStore contract so spend enforcement
is not split between getAccumulatedCost and addCost. Add an atomic reservation
operation, with idempotency support and concurrency-safe limit enforcement, and
update DefaultLedgerAdvisor.before() to reserve before provider invocation.
Reconcile the reservation with actual cost in after(), preserving final BLOCK
enforcement for concurrent requests.

In
`@token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java`:
- Around line 30-54: Update DefaultBudgetEvaluator’s evaluate methods and the
ledger flow used by DefaultLedgerAdvisor.after() so final spend control does not
rely on check-then-add: reserve the estimated cost atomically for each BudgetKey
before provider invocation, enforce BLOCK from the reservation result, and
ensure concurrent requests cannot exceed the policy limit. Make the cost-bearing
evaluation path used by before() include the current request estimate, and
preserve a clear reconciliation path for replacing the reservation with actual
cost and releasing any difference, with idempotency for retries.

---

Outside diff comments:
In
`@token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java`:
- Around line 37-60: 동일 key·threshold 알림의 중복 발행을 막도록 원자적 claim 흐름으로 변경하세요.
token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java:37-60에서는
evaluator-resolved BudgetKey로 NotificationStateStore.advanceIfHigher(...)를 호출하고
성공한 경우에만 이벤트를 발행하세요.
token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java:11-16의
계약을 해당 boolean 원자적 상태 전이로 교체하고,
token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java:16-27에서는
ConcurrentHashMap.compute 등으로 동일 window의 동등하거나 낮은 threshold를 억제하며 비교·갱신을 원자화하세요.
token-pilot-notification/src/test/java/io/tokenpilot/notification/BudgetNotificationServiceTest.java:25-52에는
같은 key·threshold의 동시 호출 시 handler가 한 번만 실행되는 회귀 테스트를 추가하세요.

In
`@token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`:
- Around line 63-88: Update the budget-processing block in
DefaultLedgerAdvisor.after to also require budgetEvaluator before calling
extractBudgetDecision, and defensively catch failures from decision extraction,
cost calculation, or budgetStateStore.addCost so pricing or budget configuration
errors are logged without preventing the original ChatClientResponse from
returning. Preserve ledgerManager.record and always return response.
- Around line 51-61: DefaultLedgerAdvisor의 before()에서
budgetEvaluator.evaluate(tags) 결과가 BudgetState.BLOCK이면 provider 호출로 진행되지 않도록 요청을
중단하거나 예외를 던지도록 수정하세요. 차단되지 않은 결정만 기존처럼 BUDGET_DECISION_CONTEXT에 저장한 변형된 요청을
반환하고, evaluator가 없는 경우의 기존 동작은 유지하세요.
🪄 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: 6935906c-ed89-4adf-a2a8-0ed2dbf0c1b5

📥 Commits

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

📒 Files selected for processing (22)
  • AGENTS.md
  • token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java
  • token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotProperties.java
  • token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetKey.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetPolicy.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java
  • token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetWindow.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-budget/src/test/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStoreTest.java
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationEvent.java
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java
  • token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java
  • token-pilot-notification/src/test/java/io/tokenpilot/notification/BudgetNotificationServiceTest.java
  • token-pilot-sample-app/src/main/java/io/tokenpilot/sample/SampleController.java
  • token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java
  • token-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisorTest.java

Comment on lines 8 to +15
/**
* 예산 판단을 위해 비용 누적 상태를 관리하는 저장소 인터페이스입니다.
* <p>
* 예산 식별자별(예: tenant)로 비용을 조회하고
* 누적하는 책임을 가집니다.
* resolved {@link BudgetKey}별 누적 비용과 limit/currency snapshot을 관리합니다.
*/

public interface BudgetStateStore {

BigDecimal getAccumulatedCost(Map<String, String> tags);
BigDecimal getAccumulatedCost(BudgetKey key, BigDecimal limit, Currency currency);

void addCost(Map<String, String> tags, BigDecimal amount);
void addCost(BudgetKey key, BigDecimal limit, Currency currency, Cost amount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

getAccumulatedCost/addCost는 여전히 check-then-add 구조입니다.

두 메서드가 분리되어 있어, 동시에 들어온 여러 요청이 모두 getAccumulatedCost로 한도 미달을 확인한 뒤 각각 addCost를 호출하면 월 한도를 초과할 수 있습니다. DefaultLedgerAdvisor에서도 before()의 평가와 after()addCost 사이에 실제 LLM 호출 시간만큼의 간격이 있어 동일한 위험이 그대로 전파됩니다. 원자적 예약(reserve)이나 idempotency 키 없이는 최종 지출 제어가 보장되지 않습니다.

이 PR의 범위(식별자/윈도우 정합성)를 벗어나는 것은 이해하지만, 이번 diff에서 인터페이스 계약 자체가 재정의되고 있어 명시적으로 짚어둡니다.

As per coding guidelines, token-pilot-budget/src/main/java/**/*.java: "Do not rely on check-then-add for final spend control; implement BLOCK enforcement before provider invocation, atomic reservation, idempotency, concurrency safety, and estimate/actual reconciliation as required MVP behavior."

🤖 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-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java`
around lines 8 - 15, Redefine the BudgetStateStore contract so spend enforcement
is not split between getAccumulatedCost and addCost. Add an atomic reservation
operation, with idempotency support and concurrency-safe limit enforcement, and
update DefaultLedgerAdvisor.before() to reserve before provider invocation.
Reconcile the reservation with actual cost in after(), preserving final BLOCK
enforcement for concurrent requests.

Source: Coding guidelines

Comment on lines 30 to +54
@Override
public BudgetDecision evaluate(
Map<String, String> tags,
BigDecimal costAmount
) {

BigDecimal currentUsage = store.getAccumulatedCost(tags);
BigDecimal newUsage = currentUsage.add(costAmount);

BigDecimal halfThreshold = monthlyLimit.multiply(BigDecimal.valueOf(0.5));
BigDecimal warningThreshold = monthlyLimit.multiply(BigDecimal.valueOf(0.8));

if (newUsage.compareTo(monthlyLimit) >= 0) {
BudgetDecision decision = new BudgetDecision(
BudgetState.BLOCK,
BudgetThreshold.EXCEEDED,
"월 예산을 초과했습니다",
newUsage,
monthlyLimit
);
public BudgetDecision evaluate(Map<String, String> tags, BigDecimal costAmount) {
BudgetKey key = resolveKey(tags);
BigDecimal usage = store.getAccumulatedCost(
key,
policy.monthlyLimit(),
policy.currency()
).add(costAmount);
BudgetDecision decision = decide(key, usage);
if (decision.state() == BudgetState.BLOCK) {
throw new BudgetExceededException(decision);
}
return decision;
}

// ✅ 80% 이상
if (newUsage.compareTo(warningThreshold) >= 0) {
return new BudgetDecision(
BudgetState.WARN,
BudgetThreshold.WARNING,
"월 예산의 80% 이상 사용",
newUsage,
monthlyLimit
);
}
@Override
public BudgetDecision evaluate(Map<String, String> tags) {
BudgetKey key = resolveKey(tags);
BigDecimal usage = store.getAccumulatedCost(
key,
policy.monthlyLimit(),
policy.currency()
);
return decide(key, usage);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

check-then-add 방식의 예산 집행 – 동시성 하에 한도 초과 가능

evaluate(tags, costAmount)는 누적 비용을 조회해 예상치를 계산하고 BLOCK 여부만 판정할 뿐, 이 값을 store에 원자적으로 반영(예약)하지 않습니다. 실제 커밋은 응답 이후 별도의 addCost 호출(그래프 컨텍스트상 DefaultLedgerAdvisor.after())에서 이뤄지므로, 두 요청이 동시에 동일 key로 evaluate()를 통과한 뒤 각자 실제 비용을 더하면 총합이 한도를 초과할 수 있습니다(TOCTOU). 또한 before()에서 쓰는 evaluate(tags)(비용 없는 오버로드)는 현재 요청의 예상 비용조차 반영하지 않아, 매 요청이 "이전 누적치"만으로 판정됩니다.

추가된 동시성 테스트는 key 생성의 결정성만 검증하며, 실제 예산 집행의 원자성은 검증하지 않습니다.

As per coding guidelines, "Do not rely on check-then-add for final spend control; implement BLOCK enforcement before provider invocation, atomic reservation, idempotency, concurrency safety, and estimate/actual reconciliation as required MVP behavior."

🤖 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-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java`
around lines 30 - 54, Update DefaultBudgetEvaluator’s evaluate methods and the
ledger flow used by DefaultLedgerAdvisor.after() so final spend control does not
rely on check-then-add: reserve the estimated cost atomically for each BudgetKey
before provider invocation, enforce BLOCK from the reservation result, and
ensure concurrent requests cannot exceed the policy limit. Make the cost-bearing
evaluation path used by before() include the current request estimate, and
preserve a clear reconciliation path for replacing the reservation with actual
cost and releasing any difference, with idempotency for retries.

Source: Coding guidelines

@HuitaePark

Copy link
Copy Markdown
Member

@Leejaewang03 고생하셨습니다!

@HuitaePark
HuitaePark merged commit 16dea43 into main Jul 24, 2026
2 checks passed
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.

2 participants