[Pricing] missing pricing·explicit zero와 pricing snapshot 정책 구현 - #55
Conversation
…ing snapshot을 사용한다.
… snapshot을 암묵적으로 적용하지 않고 reconciliation required 결과로 처리한다.
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughSummary by CodeRabbit
WalkthroughChanges가격 누락 정책과 typed resolution을 추가했습니다. 정책별 가격 조회와 immutable pricing snapshot을 구현했습니다. 플랜·스냅샷 기반 비용 기록과 Spring AI advisor의 가격 해석 및 비용 처리
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
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
PricingSnapshot→PricingPlan변환이 두 모듈에 중복 구현되어 있습니다. 근본 원인은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
📒 Files selected for processing (23)
token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.javatoken-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.javatoken-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.javatoken-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/MissingPricingPolicy.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingRateFallback.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingResolution.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.javatoken-pilot-core/src/main/java/io/tokenpilot/core/exception/MissingPricingException.javatoken-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.javatoken-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/MissingPricingPolicyTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingPlanTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingResolutionTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/exception/MissingPricingExceptionTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/internal/DefaultLedgerManagerTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/internal/InMemoryPricingRegistryTest.javatoken-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.javatoken-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.javatoken-pilot-spring-ai/src/test/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisorTest.java
| if (snapshot.isPresent()) { | ||
| if (!snapshot.get().modelId().equals(responseModelId)) { | ||
| return withReconciliationResult(response, PricingReconciliationResult.RECONCILIATION_REQUIRED); | ||
| } |
There was a problem hiding this comment.
🗄️ 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.javatoken-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingPlanTest.javatoken-pilot-core/src/test/java/io/tokenpilot/core/domain/PricingSnapshotTest.javatoken-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.javatoken-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
There was a problem hiding this comment.
typed resolution과 요청 단위 snapshot 방향은 맞습니다. 다만 현재 구현에는 missing pricing을 다시 0원으로 흡수하는 경로가 남아 있어 이 상태로는 병합하기 어렵습니다.
-
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로 남는 회귀 테스트가 필요합니다. -
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을 연결해 주세요. -
resolution/reconciliation 정책이
DefaultLedgerAdvisor의 private 로직에 있어 plain-Java core와 후속 #45/#36/#37이 동일 계약을 재사용하기 어렵습니다. 최소한 snapshot rate 검증과 reconciliation 판단은 core 경계로 내리고 Spring adapter는 request/response 변환만 담당하는 편이 안전합니다.
추가로 #54와 DefaultLedgerAdvisor 및 테스트에서 실제 충돌이 납니다. #54 병합 후 rebase하면서 BLOCK 선차단 로직이 유지되는지 회귀 테스트로 확인해 주세요.
해당 부분은 언급대로 #54의 머지 이후 rebase 하여 확인하겠습니다! |
변경 사항
missing pricing과 explicit zero pricing을 구분하는 typed pricing policy 경계를 추가했습니다.
PricingResolution으로RESOLVED,MISSING_PLAN,MISSING_RATE,CURRENCY_MISMATCH상태를 표현합니다.PricingPlan.resolveRate(...)를 통해 rate 누락을BigDecimal.ZEROfallback으로 숨기지 않고MISSING_RATE로 표현합니다.0rate는 정상RESOLVED로 유지합니다.RESOLVED로 처리합니다.PricingSnapshot을 추가해 provider 호출 전 resolve된 pricing 정보를 요청 단위 immutable 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을UNPRICEDreconciliation result로 남깁니다.FAIL_OPENmissing pricing 경로에서는 ledger actual cost를0원으로 기록하지 않습니다.UNPRICED는PricingResolution이 아니라PricingReconciliationResult에만 추가했습니다.UNPRICED로 처리되어도 원래PricingResolution이 context에 유지되도록 했습니다.RESOLVED결과와RECONCILED정산 경로를 타도록 테스트를 추가했습니다.FAIL_CLOSED로 설정했습니다.FAIL_OPEN을 유지합니다.PricingResolution.name()을 metric/event에서 사용할 수 있는 low-cardinality 값으로 고정했습니다.배경
기존 흐름에서는 가격 정보 누락과 명시적인 무료 가격이 모두 0원처럼 보일 수 있었습니다.
특히 다음 문제가 있었습니다.
Optional.empty()로만 표현됨BigDecimal.ZERO처럼 보일 수 있음이번 변경은 다음 정책 계약을 구현합니다.
정책별 동작
FAIL_CLOSEDFAIL_OPENexplicit zerocurrency mismatch호환성
DefaultLedgerAdvisor생성자는 ledger-only 기본 동작으로FAIL_OPEN을 유지합니다.FAIL_CLOSED를 명시적으로 사용합니다.MissingPricingPolicy를 명시적으로 받는 생성자에null이 들어오면 즉시 실패합니다.PricingResolution에는UNPRICED나RECONCILIATION_REQUIRED를 추가하지 않았습니다.UNPRICED와RECONCILIATION_REQUIRED는 actual reconciliation 결과이므로PricingReconciliationResult에만 존재합니다.범위 메모
modelId를 입력으로 제공한다는 전제만 둡니다.PricingResolution/PricingSnapshot을 재사용할 수 있는 core contract를 제공하는 수준입니다.REQUIRED_TOKEN_TYPE_CONTEXT는 provider 호출 전MISSING_RATE를 판단하기 위한 최소 입력 경계입니다. 후속 preflight/request adapter가 생기면 그 결과 입력으로 대체할 수 있습니다.이후 과제
[Core] versioned ModelRegistry와 최소 모델 정책 등록 #32 alias/canonical model registry 연결
modelId를 입력으로 받는 전제만 둡니다.[Core] 보수적 PreflightCostBound 계산 계약과 구현 #45/[Budget] atomic reservation store와 idempotency 구현 #36/#37의 동일 resolution/snapshot 재사용
PricingResolution과PricingSnapshotcontract를 제공합니다.notification lifecycle은 #48에서 분리 구현
체크리스트
필수 테스트
MISSING_PLAN이다.MISSING_RATE다.0rate는RESOLVED zero cost다.FAIL_CLOSED에서 provider 호출 횟수가0이다.FAIL_OPEN은 호출되지만0원 actual로 기록되지 않는다.modelId로 resolve한 입력은 동일 pricing policy snapshot을 사용한다.Acceptance Criteria
FAIL_CLOSED다.PricingResolution/PricingSnapshotcontract를 제공한다.Closes #28