Skip to content

[Pricing] missing pricing·explicit zero와 pricing snapshot 정책 구현 - #55

Merged
HuitaePark merged 33 commits into
tokenpliot:mainfrom
Rigu1:feat/missing-pricing-and-pricing-snapshot
Aug 4, 2026
Merged

[Pricing] missing pricing·explicit zero와 pricing snapshot 정책 구현#55
HuitaePark merged 33 commits into
tokenpliot:mainfrom
Rigu1:feat/missing-pricing-and-pricing-snapshot

Conversation

@Rigu1

@Rigu1 Rigu1 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

변경 사항

missing pricing과 explicit zero pricing을 구분하는 typed pricing policy 경계를 추가했습니다.

  • PricingResolution으로 RESOLVED, MISSING_PLAN, MISSING_RATE, CURRENCY_MISMATCH 상태를 표현합니다.
  • PricingPlan.resolveRate(...)를 통해 rate 누락을 BigDecimal.ZERO fallback으로 숨기지 않고 MISSING_RATE로 표현합니다.
  • 명시적으로 등록된 0 rate는 정상 RESOLVED로 유지합니다.
  • reasoning/cache fallback은 기준 rate가 명시적으로 존재할 때만 RESOLVED로 처리합니다.
  • PricingSnapshot을 추가해 provider 호출 전 resolve된 pricing 정보를 요청 단위 immutable snapshot으로 보존합니다.
  • actual reconciliation은 registry current plan이 아니라 요청 시점 snapshot을 사용합니다.
  • response model이 snapshot model과 다르면 기존 snapshot을 자동 적용하지 않고 RECONCILIATION_REQUIRED로 처리합니다.
  • MissingPricingPolicy.FAIL_CLOSED에서 missing pricing이면 provider 호출 전에 MissingPricingException으로 차단합니다.
  • FAIL_CLOSED 차단 예외는 PricingResolution을 구조화된 값으로 보존합니다.
  • FAIL_CLOSED에서 MISSING_PLAN, MISSING_RATE 모두 provider invocation count가 0임을 검증했습니다.
  • MissingPricingPolicy.FAIL_OPEN에서는 provider 호출을 허용하되 missing pricing을 UNPRICED reconciliation result로 남깁니다.
  • FAIL_OPEN missing pricing 경로에서는 ledger actual cost를 0원으로 기록하지 않습니다.
  • UNPRICEDPricingResolution이 아니라 PricingReconciliationResult에만 추가했습니다.
  • missing pricing이 UNPRICED로 처리되어도 원래 PricingResolution이 context에 유지되도록 했습니다.
  • explicit zero pricing은 정상 RESOLVED 결과와 RECONCILED 정산 경로를 타도록 테스트를 추가했습니다.
  • budget-aware autoconfigure 경계에서는 기본 missing pricing policy를 FAIL_CLOSED로 설정했습니다.
  • 기존 ledger-only advisor 기본 경로는 FAIL_OPEN을 유지합니다.
  • pricing miss reason은 PricingResolution.name()을 metric/event에서 사용할 수 있는 low-cardinality 값으로 고정했습니다.

배경

기존 흐름에서는 가격 정보 누락과 명시적인 무료 가격이 모두 0원처럼 보일 수 있었습니다.

특히 다음 문제가 있었습니다.

  • 미등록 model plan은 Optional.empty()로만 표현됨
  • token type rate 누락은 fallback 끝에서 BigDecimal.ZERO처럼 보일 수 있음
  • ledger-only 경로에서 plan 없음이 실제 무료 가격과 구분되기 어려움
  • 요청 중 registry가 변경되면 reservation과 actual reconciliation이 서로 다른 가격을 사용할 수 있음

이번 변경은 다음 정책 계약을 구현합니다.

explicit zero price != missing price

PricingResolution
├─ RESOLVED
├─ MISSING_PLAN
├─ MISSING_RATE
└─ CURRENCY_MISMATCH

FAIL_CLOSED: provider 호출 전에 구조화된 PricingResolution으로 차단하고 invocation count는 0
FAIL_OPEN: provider 호출은 허용하지만 UNPRICED signal을 남기며 0원으로 정산하지 않음
explicit zero: 정상 RESOLVED 결과와 0원 cost
missing price: #45 CostBound 생성 실패로 전파 가능한 typed value로 보존

정책별 동작

FAIL_CLOSED

MISSING_PLAN / MISSING_RATE
→ MissingPricingException
→ getResolution() == PricingResolution.MISSING_PLAN 또는 MISSING_RATE
→ provider invocation count == 0

FAIL_OPEN

MISSING_PLAN
→ provider 호출 허용
→ PricingReconciliationResult.UNPRICED
→ ledger actual cost 미기록
→ PricingResolution.MISSING_PLAN 유지

explicit zero

명시적 0 rate snapshot
→ PricingResolution.RESOLVED
→ provider 호출 허용
→ PricingReconciliationResult.RECONCILED
→ Cost.zero(currency) 정산

currency mismatch

expected currency != plan currency
→ PricingResolution.CURRENCY_MISMATCH
→ isResolved() == false
→ 등록된 pricing plan/snapshot 상태는 변경되지 않음

호환성

  • 기존 DefaultLedgerAdvisor 생성자는 ledger-only 기본 동작으로 FAIL_OPEN을 유지합니다.
  • budget-aware autoconfigure 경계에서는 FAIL_CLOSED를 명시적으로 사용합니다.
  • MissingPricingPolicy를 명시적으로 받는 생성자에 null이 들어오면 즉시 실패합니다.
  • PricingResolution에는 UNPRICEDRECONCILIATION_REQUIRED를 추가하지 않았습니다.
  • UNPRICEDRECONCILIATION_REQUIRED는 actual reconciliation 결과이므로 PricingReconciliationResult에만 존재합니다.
  • alias resolver와 canonical model registry는 [Core] versioned ModelRegistry와 최소 모델 정책 등록 #32 범위이므로 구현하지 않았습니다.
  • notification lifecycle은 이번 이슈에서 구현하지 않고 #48로 분리합니다.

범위 메모

  • #32가 alias를 resolve한 modelId를 입력으로 제공한다는 전제만 둡니다.
  • alias resolver 자체는 구현하지 않았습니다.
  • [Core] 보수적 PreflightCostBound 계산 계약과 구현 #45/[Budget] atomic reservation store와 idempotency 구현 #36/#37은 아직 구현 전이므로, 이번 PR에서는 동일 PricingResolution/PricingSnapshot을 재사용할 수 있는 core contract를 제공하는 수준입니다.
  • REQUIRED_TOKEN_TYPE_CONTEXT는 provider 호출 전 MISSING_RATE를 판단하기 위한 최소 입력 경계입니다. 후속 preflight/request adapter가 생기면 그 결과 입력으로 대체할 수 있습니다.

이후 과제

  1. [Core] versioned ModelRegistry와 최소 모델 정책 등록 #32 alias/canonical model registry 연결

    • 이번 PR은 #32가 이미 resolve한 modelId를 입력으로 받는 전제만 둡니다.
    • alias resolver와 canonical model registry가 구현되면 alias와 canonical model이 실제로 동일 pricing policy snapshot을 사용하는지 resolver 경계에서 보강합니다.
  2. [Core] 보수적 PreflightCostBound 계산 계약과 구현 #45/[Budget] atomic reservation store와 idempotency 구현 #36/#37의 동일 resolution/snapshot 재사용

  3. notification lifecycle은 #48에서 분리 구현

    • 이번 PR은 missing pricing과 snapshot 정책만 다룹니다.
    • pricing miss, unpriced, reconciliation required에 대한 notification lifecycle은 #48에서 별도 구현합니다.

체크리스트

필수 테스트

  • 미등록 plan은 MISSING_PLAN이다.
  • token type rate 누락은 MISSING_RATE다.
  • 명시적 0 rate는 RESOLVED zero cost다.
  • 명시적 base rate가 있을 때만 reasoning/cache fallback이 동작한다.
  • FAIL_CLOSED에서 provider 호출 횟수가 0이다.
  • FAIL_OPEN은 호출되지만 0원 actual로 기록되지 않는다.
  • #32가 같은 modelId로 resolve한 입력은 동일 pricing policy snapshot을 사용한다.
  • registry 변경 후에도 in-flight 요청은 기존 snapshot으로 reconcile한다.
  • currency mismatch는 상태 무변경으로 실패한다.
  • pricing miss reason을 metric/event가 low-cardinality 값으로 식별할 수 있다.

Acceptance Criteria

Closes #28

Rigu1 added 23 commits July 30, 2026 06:59
… snapshot을 암묵적으로 적용하지 않고 reconciliation required 결과로 처리한다.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 917497d4-3a3e-4d0d-b12f-f8ec72048627

📥 Commits

Reviewing files that changed from the base of the PR and between 8788042 and 12ecaf5.

📒 Files selected for processing (4)
  • AGENTS.md
  • token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.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
🚧 Files skipped from review as they are similar to previous changes (3)
  • token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java
  • token-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisorTest.java
  • token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java

📝 Walkthrough

Summary by CodeRabbit

  • 신규 기능
    • 가격 정책 ID별 요금 조회와 요청 시점의 요금 스냅샷 기반 비용 산정을 지원합니다.
    • 요금 해석 상태와 모델 요금 재조정 결과를 제공합니다.
    • 누락 가격 처리 정책으로 FAIL_OPEN/FAIL_CLOSED를 제공합니다.
  • 개선 사항
    • FAIL_CLOSED에서는 가격 확인이 불가능한 요청을 차단하고 해석 상태를 안내합니다.
    • 토큰 타입별 요율 폴백, 기대 통화 불일치, 명시적 무료 요율 처리가 강화되었습니다.
    • 응답 모델 요금 불일치와 미가격 비용을 구분해 처리합니다.

Walkthrough

Changes

가격 누락 정책과 typed resolution을 추가했습니다. 정책별 가격 조회와 immutable pricing snapshot을 구현했습니다. 플랜·스냅샷 기반 비용 기록과 Spring AI advisor의 FAIL_OPEN/FAIL_CLOSED 처리를 연결했습니다. 자동 구성과 통합 테스트를 확장했습니다.

가격 해석 및 비용 처리

Layer / File(s) Summary
가격 도메인 계약과 해상도
token-pilot-core/src/main/java/io/tokenpilot/core/domain/*, token-pilot-core/src/main/java/io/tokenpilot/core/exception/*, token-pilot-core/src/main/java/io/tokenpilot/core/PricingEvaluator.java, token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultPricingEvaluator.java, token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java, token-pilot-core/src/test/java/io/tokenpilot/core/domain/*, token-pilot-core/src/test/java/io/tokenpilot/core/exception/*, token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultPricingEvaluatorTest.java, token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultCostCalculatorTest.java
PricingPlan에 pricing policy와 명시적 rate 해상도를 추가했습니다. PricingSnapshot, PricingResolution, PricingReconciliationResult, MissingPricingPolicy, MissingPricingException, PricingEvaluator를 추가했습니다. 비용 계산은 해석되지 않은 rate에서 구조화된 예외를 발생시킵니다.
정책별 registry 조회와 rate 해상도
token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java, token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java, token-pilot-core/src/test/java/io/tokenpilot/core/internal/InMemoryPricingRegistryTest.java
모델·정책 조합 조회, 요청 단위 snapshot 생성, rate 누락 및 통화 불일치 해상도를 구현하고 테스트했습니다.
플랜·스냅샷 기반 비용 기록
token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java, token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java, token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultLedgerManagerTest.java
LedgerManagerPricingPlanPricingSnapshot 기반 비용 기록을 지원합니다. 계산 결과와 기록 이벤트를 공통 경로로 발행합니다.
Advisor의 missing pricing 정책 처리
token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java, token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java, token-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisorTest.java
Advisor가 provider 호출 전에 snapshot을 해석합니다. FAIL_OPEN 또는 FAIL_CLOSED에 따라 provider 호출, 비용 정산, 예외 처리를 수행합니다. 응답 모델 불일치와 missing pricing 결과도 컨텍스트에 저장합니다.
자동 구성과 통합 검증
token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java, token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java, token-pilot-sample-app/src/test/java/io/tokenpilot/sample/SampleApplicationChatClientE2ETest.java, AGENTS.md
자동 구성에 기본 PricingEvaluator를 등록하고 advisor 생성 경로에 pricing 의존성과 정책을 연결했습니다. 자동 구성, 샘플 애플리케이션 및 구현 상태 문서에서 snapshot, resolution, reconciliation, 누적 비용을 검증하고 기술했습니다.

Suggested reviewers: huitaepark

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.76% 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 제목은 missing pricing, explicit zero, pricing snapshot 정책 구현이라는 주요 변경을 명확하게 요약합니다.
Description check ✅ Passed 설명은 typed pricing resolution, snapshot, FAIL_OPEN·FAIL_CLOSED 정책과 테스트 범위를 변경 사항과 직접 연결해 설명합니다.
Linked Issues check ✅ Passed 직접 연결된 이슈 #28의 typed pricing, snapshot, reconciliation, missing-pricing 정책 요구사항을 구현하고 후속 이슈 범위는 제외했습니다.
Out of Scope Changes check ✅ Passed 변경된 구현, 테스트, 문서는 이슈 #28의 pricing resolution 및 snapshot 정책 범위와 관련되며 명확한 범위 외 변경은 없습니다.

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

@coderabbitai
coderabbitai Bot requested a review from HuitaePark July 30, 2026 08:29

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

🧹 Nitpick comments (7)
token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.java (1)

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

음수 rate 검증에 대한 테스트 커버리지 없음.

컴팩트 생성자의 rate must not be negative 검증 경로가 이 테스트 파일에서 전혀 검증되지 않습니다. 금액 관련 불변식이므로 회귀 방지를 위해 테스트 추가를 권장합니다.

✅ 제안 테스트
+    `@Test`
+    `@DisplayName`("음수 rate는 pricing snapshot 생성 시 거부되어야 한다")
+    void rejectsNegativeRate() {
+        Map<TokenType, BigDecimal> rates = new EnumMap<>(TokenType.class);
+        rates.put(TokenType.PROMPT, new BigDecimal("-0.01"));
+
+        assertThatThrownBy(() -> new PricingSnapshot(
+                "gpt-4o",
+                "standard",
+                "catalog-v1",
+                Instant.parse("2026-07-30T00:00:00Z"),
+                rates,
+                Currency.getInstance("USD")
+        )).isInstanceOf(IllegalArgumentException.class)
+          .hasMessage("rate must not be negative");
+    }
🤖 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/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.java`
around lines 1 - 65, PricingSnapshotTest에 음수 rate 입력을 거부하는 생성자 검증 테스트를 추가하세요.
rates에 음수 BigDecimal을 포함한 PricingSnapshot 생성을 assertThatThrownBy로 감싸고, compact
constructor의 기존 예외 타입과 메시지에 맞게 검증하여 `rate must not be negative` 불변식을 고정하세요.
token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java (2)

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

내부 tokenpilot.* context 키가 ledger 태그로 승격됩니다.

extractTags는 String 값을 가진 모든 context 항목을 태그로 수집하므로 MODEL_ID_CONTEXT, PRICING_POLICY_ID_CONTEXT가 태그 맵에 섞여 리스너/메트릭 태그 카디널리티를 오염시킵니다. 내부 예약 접두어는 제외하는 것이 안전합니다.

♻️ 제안
         context.forEach((k, v) -> {
-            if (v instanceof String s) {
+            if (v instanceof String s && !k.startsWith("tokenpilot.")) {
                 tags.put(k, s);
             }
         });
🤖 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 325 - 337, Update extractTags to exclude context entries whose keys
use the internal reserved tokenpilot.* prefix before adding String values to the
tags map. Continue collecting non-internal String-valued context entries and
preserve the existing null-context behavior.

261-269: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

응답 metadata에 모델이 없으면 모델 변경을 감지할 수 없습니다.

extractResponseModelId는 metadata가 비면 extractModelId(response)로 폴백하는데, 이 값은 요청 시 주입한 MODEL_ID_CONTEXT입니다. 결과적으로 Line 109의 비교가 항상 일치해 스냅샷이 무조건 적용됩니다. metadata 부재 시에는 비교를 건너뛰는 것이 아니라 RECONCILIATION_REQUIRED로 처리하는 등 명시적 결정이 필요합니다.

🤖 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 261 - 269, Update extractResponseModelId and the Line 109
model-comparison flow so missing response metadata is not replaced with the
request-scoped MODEL_ID_CONTEXT via extractModelId(response). Represent the
metadata-absent case explicitly and route it to the existing
RECONCILIATION_REQUIRED handling, rather than treating the model as matching and
unconditionally applying the snapshot.
token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java (1)

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

컨텍스트 키가 문자열 리터럴로 하드코딩되어 있습니다.

DefaultLedgerAdvisor.MODEL_ID_CONTEXT가 변경되면 이 테스트는 예외 대신 조용히 다른 경로를 타게 됩니다. 해당 상수를 public API로 승격해 참조하거나, 최소한 키의 출처를 주석으로 남겨주세요.

As per coding guidelines, "register beans by public interface type whenever possible" — 컨텍스트 키 역시 공개 계약으로 노출하는 편이 모듈 간 결합을 명시화합니다.

🤖 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-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java`
around lines 277 - 283, Update the ChatClientRequest context map in
TokenPilotAutoConfigurationTest to use the public
DefaultLedgerAdvisor.MODEL_ID_CONTEXT constant instead of hardcoding
"tokenpilot.model.id". If that constant is not publicly accessible, expose it as
a public API before referencing it in the test.

Source: Coding guidelines

token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java (1)

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

PricingSnapshotPricingPlan 변환이 두 모듈에 중복 구현되어 있습니다. 근본 원인은 PricingSnapshot에 변환 팩토리가 없어 각 호출부가 4개 필드를 직접 재조립한다는 점이며, 스냅샷 필드가 추가되면 두 구현이 조용히 어긋납니다.

  • token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java#L62-L71: 인라인 new PricingPlan(...)snapshot.toPricingPlan() 호출로 교체하고, PricingSnapshot에 해당 팩토리를 추가하세요.
  • token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java#L213-L221: resolveSnapshotRate도 동일 팩토리를 사용해 spring-ai 모듈이 가격 조립 로직을 재현하지 않도록 하세요.

As per coding guidelines, "Translate Spring AI requests and Usage values into Token Pilot core interfaces without duplicating policy or accounting logic."

🤖 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/internal/DefaultLedgerManager.java`
around lines 62 - 71, The PricingSnapshot-to-PricingPlan construction is
duplicated across modules. In
token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java:62-71,
add a conversion factory on PricingSnapshot and replace the inline PricingPlan
construction in record with snapshot.toPricingPlan(); in
token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java:213-221,
update resolveSnapshotRate to use the same factory instead of reassembling
pricing fields.

Source: Coding guidelines

token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java (1)

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

FAIL_CLOSED가 하드코딩되어 사용자가 완화할 수 없습니다.

이 기본값은 합리적이지만, 가격 정책이 아직 등록되지 않은 환경에서는 budget 활성화만으로 모든 호출이 MissingPricingException으로 차단됩니다. token-pilot.pricing.missing-policy 같은 프로퍼티로 노출해 운영자가 FAIL_OPEN으로 되돌릴 수 있게 하는 편이 안전합니다.

프로퍼티 바인딩 초안이 필요하시면 알려주세요.

🤖 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-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java`
around lines 118 - 126, TokenPilotAutoConfiguration의 defaultLedgerAdvisor 호출에서
MissingPricingPolicy.FAIL_CLOSED를 하드코딩하지 말고, token-pilot.pricing.missing-policy
프로퍼티로 바인딩된 설정값을 사용하세요. 기본값은 FAIL_CLOSED로 유지하면서 운영자가 FAIL_OPEN을 선택할 수 있도록 관련 설정
프로퍼티와 바인딩을 추가하고, 해당 값을 advisor 생성 인자에 전달하세요.
token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingResolutionTest.java (1)

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

대소문자 무시 비교로 바꾸세요

extracting(PricingResolution::name) 결과에 소문자 리터럴만 검사해서, MODEL_MISSING 같은 대문자 enum 이름 추가를 잡지 못합니다. doesNotContainIgnoringCase(...)로 바꾸면 의도가 유지됩니다.

🤖 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/test/java/io/tokenpilot/core/domain/PricingResolutionTest.java`
around lines 73 - 85, Update
pricingMissReasonDoesNotContainHighCardinalityIdentifiers to use
case-insensitive assertions for every forbidden identifier, replacing the
current case-sensitive doesNotContain checks with doesNotContainIgnoringCase
while preserving the existing filtered PricingResolution values and literals.
🤖 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 19-40: Update the compact constructors of PricingPlan in
token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java:19-40
and PricingSnapshot in
token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java:24-43
to requireNonNull(rates, ...), then create an EnumMap with TokenType.class and
populate it via putAll before wrapping it as unmodifiable. Preserve the existing
validation and currency behavior.

In
`@token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`:
- Around line 108-111: Update the model-mismatch branch in DefaultLedgerAdvisor
before withReconciliationResult so the response’s token usage is recorded as
UNPRICED without assigning a cost, or emit the established usage-loss
observability signal. Preserve the RECONCILIATION_REQUIRED result while ensuring
the call’s usage remains detectable and is not silently discarded.
- Around line 168-180: Update resolvePricing in DefaultLedgerAdvisor so requests
with a null modelId are handled as missing pricing under FAIL_CLOSED: invoke the
existing rejection path and prevent the request from continuing to the legacy
unpriced flow. Preserve the current behavior for policies that allow missing
pricing, and ensure permitted requests still receive the appropriate pricing
context.

---

Nitpick comments:
In
`@token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java`:
- Around line 118-126: TokenPilotAutoConfiguration의 defaultLedgerAdvisor 호출에서
MissingPricingPolicy.FAIL_CLOSED를 하드코딩하지 말고, token-pilot.pricing.missing-policy
프로퍼티로 바인딩된 설정값을 사용하세요. 기본값은 FAIL_CLOSED로 유지하면서 운영자가 FAIL_OPEN을 선택할 수 있도록 관련 설정
프로퍼티와 바인딩을 추가하고, 해당 값을 advisor 생성 인자에 전달하세요.

In
`@token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java`:
- Around line 277-283: Update the ChatClientRequest context map in
TokenPilotAutoConfigurationTest to use the public
DefaultLedgerAdvisor.MODEL_ID_CONTEXT constant instead of hardcoding
"tokenpilot.model.id". If that constant is not publicly accessible, expose it as
a public API before referencing it in the test.

In
`@token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java`:
- Around line 62-71: The PricingSnapshot-to-PricingPlan construction is
duplicated across modules. In
token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java:62-71,
add a conversion factory on PricingSnapshot and replace the inline PricingPlan
construction in record with snapshot.toPricingPlan(); in
token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java:213-221,
update resolveSnapshotRate to use the same factory instead of reassembling
pricing fields.

In
`@token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingResolutionTest.java`:
- Around line 73-85: Update
pricingMissReasonDoesNotContainHighCardinalityIdentifiers to use
case-insensitive assertions for every forbidden identifier, replacing the
current case-sensitive doesNotContain checks with doesNotContainIgnoringCase
while preserving the existing filtered PricingResolution values and literals.

In
`@token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.java`:
- Around line 1-65: PricingSnapshotTest에 음수 rate 입력을 거부하는 생성자 검증 테스트를 추가하세요.
rates에 음수 BigDecimal을 포함한 PricingSnapshot 생성을 assertThatThrownBy로 감싸고, compact
constructor의 기존 예외 타입과 메시지에 맞게 검증하여 `rate must not be negative` 불변식을 고정하세요.

In
`@token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`:
- Around line 325-337: Update extractTags to exclude context entries whose keys
use the internal reserved tokenpilot.* prefix before adding String values to the
tags map. Continue collecting non-internal String-valued context entries and
preserve the existing null-context behavior.
- Around line 261-269: Update extractResponseModelId and the Line 109
model-comparison flow so missing response metadata is not replaced with the
request-scoped MODEL_ID_CONTEXT via extractModelId(response). Represent the
metadata-absent case explicitly and route it to the existing
RECONCILIATION_REQUIRED handling, rather than treating the model as matching and
unconditionally applying the snapshot.
🪄 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: c35c186a-949e-48ce-b35a-185a9ae739d9

📥 Commits

Reviewing files that changed from the base of the PR and between 776f788 and fede723.

📒 Files selected for processing (23)
  • token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java
  • token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/MissingPricingPolicy.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingRateFallback.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingResolution.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/exception/MissingPricingException.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/MissingPricingPolicyTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingPlanTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingResolutionTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/exception/MissingPricingExceptionTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultLedgerManagerTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/internal/InMemoryPricingRegistryTest.java
  • token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java
  • token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java
  • token-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisorTest.java

Comment on lines +108 to +111
if (snapshot.isPresent()) {
if (!snapshot.get().modelId().equals(responseModelId)) {
return withReconciliationResult(response, PricingReconciliationResult.RECONCILIATION_REQUIRED);
}

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

RECONCILIATION_REQUIRED 경로에서 사용량이 전혀 기록되지 않습니다.

응답 모델이 스냅샷 모델과 다르면 ledger 기록도, budget 누적도 없이 반환되므로 해당 호출의 토큰 사용량과 비용이 조용히 유실됩니다. 후속 정산으로 넘기는 설계 의도는 이해되지만, 최소한 사용량은 UNPRICED처럼 기록하거나(비용 0 없이) 별도 관측 신호를 남겨야 유실을 탐지할 수 있습니다.

후속 이슈로 추적할지, 아니면 이 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-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`
around lines 108 - 111, Update the model-mismatch branch in DefaultLedgerAdvisor
before withReconciliationResult so the response’s token usage is recorded as
UNPRICED without assigning a cost, or emit the established usage-loss
observability signal. Preserve the RECONCILIATION_REQUIRED result while ensuring
the call’s usage remains detectable and is not silently discarded.

@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-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java`:
- Around line 171-172: Update the FAIL_OPEN path in DefaultLedgerAdvisor so
missing pricing preserves pricing context before returning the request. After
rejectMissingPricingIfFailClosed(PricingResolution.MISSING_PLAN), inject
PricingResolution.MISSING_PLAN with an empty snapshot through
withPricingContext, ensuring after() treats the call as unpriced rather than
legacy.
🪄 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: 2491dd9e-d213-43fe-a9de-4e25a86bdc08

📥 Commits

Reviewing files that changed from the base of the PR and between fede723 and a160f41.

📒 Files selected for processing (6)
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingPlanTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.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
🚧 Files skipped from review as they are similar to previous changes (2)
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java

@HuitaePark HuitaePark self-assigned this Jul 30, 2026

@HuitaePark HuitaePark left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

typed resolution과 요청 단위 snapshot 방향은 맞습니다. 다만 현재 구현에는 missing pricing을 다시 0원으로 흡수하는 경로가 남아 있어 이 상태로는 병합하기 어렵습니다.

  1. resolvePricingResolution()REQUIRED_TOKEN_TYPE_CONTEXT가 없으면 부분 rates snapshot도 RESOLVED로 처리하고, 있어도 한 종류만 검사합니다. 반면 actual 계산은 PROMPT/COMPLETION/cache/reasoning을 모두 getRate()로 계산하며 최종 누락은 0을 반환합니다. completion rate가 없는 plan에 input/output usage를 넣었을 때 RECONCILED와 0원 completion 비용이 기록되는 것을 별도 진단 테스트로 재현했습니다. actual usage에 필요한 rate가 하나라도 없으면 MISSING_RATE/UNPRICED 또는 reconciliation failure로 남는 회귀 테스트가 필요합니다.

  2. autoconfigure의 ledger-only 경로는 2-인자 advisor를 생성해 PricingRegistry를 전달하지 않으므로 pre-resolution, snapshot, UNPRICED가 실행되지 않고 기존 zero-cost 경로로 돌아갑니다. 반대로 budget 경로는 tokenpilot.model.id를 요구하지만 실제 ChatClient adapter에서 이 context를 채우는 코드가 없습니다. 테스트에서 문자열을 직접 주입하는 대신, 일반 ChatClient 호출로 model/policy가 resolve되는 E2E를 추가하고 실제 wiring을 연결해 주세요.

  3. resolution/reconciliation 정책이 DefaultLedgerAdvisor의 private 로직에 있어 plain-Java core와 후속 #45/#36/#37이 동일 계약을 재사용하기 어렵습니다. 최소한 snapshot rate 검증과 reconciliation 판단은 core 경계로 내리고 Spring adapter는 request/response 변환만 담당하는 편이 안전합니다.

추가로 #54와 DefaultLedgerAdvisor 및 테스트에서 실제 충돌이 납니다. #54 병합 후 rebase하면서 BLOCK 선차단 로직이 유지되는지 회귀 테스트로 확인해 주세요.

@coderabbitai
coderabbitai Bot requested a review from HuitaePark August 3, 2026 17:35
@Rigu1

Rigu1 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

추가로 #54와 DefaultLedgerAdvisor 및 테스트에서 실제 충돌이 납니다. #54 병합 후 rebase하면서 BLOCK 선차단 로직이 유지되는지 회귀 테스트로 확인해 주세요.

해당 부분은 언급대로 #54의 머지 이후 rebase 하여 확인하겠습니다!

@HuitaePark
HuitaePark merged commit 100f5ab into tokenpliot:main Aug 4, 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.

[Pricing] missing pricing·explicit zero와 pricing snapshot 정책 구현

2 participants