Skip to content

[Core] 보수적 PreflightCostBound 계산 계약과 구현 #45

Description

@HuitaePark

목적

호출 전 계산한 REQUEST 범위의 안전한 input token 상한reservedOutputTokens를 immutable pricing policy에 적용해, atomic budget reservation에 사용할 보수적 금액 상한을 계산한다.

이 이슈는 token context admission(#33)과 금액 reservation(#36) 사이의 명시적인 경계다. token 상한을 비용 상한으로 바꾸는 책임을 Advisor나 reservation store에 흩어놓지 않는다.

문제 정의

  • [Core] framework-independent TokenEstimator와 TokenCountResult API 추가 #30/#33은 token 상한과 context 적합 여부를 정의하지만 이를 얼마로 예약할지는 정의하지 않는다.
  • 평균 token estimate에 평균 단가를 곱한 값은 정보 제공용 추정치일 뿐 예산 초과를 막는 안전한 금액 상한이 아니다.
  • 호출 전에는 cache read/create 여부와 reasoning output 비율을 모를 수 있다.
  • 가격 미등록을 0원으로 처리하면 무료 모델과 catalog 누락을 구분할 수 없다.
  • TEXT_ONLY 계산은 request framing/tool/schema가 빠져 있으므로 전체 요청의 비용 상한이 아니다.
  • 호출 후 actual TokenUsage와 호출 전 preflight bound를 같은 타입으로 재사용하면 정산과 예약의 의미가 섞인다.

목표 API

이름은 구현 중 조정할 수 있으나 책임은 하나의 Core 서비스로 둔다.

public interface PreflightCostEstimator {
    PreflightCostResult estimate(
        ModelDefinition model,
        TokenCountResult requestInput,
        long reservedOutputTokens
    );
}

권장 결과:

PreflightCostResult
├─ Bounded
│  ├─ estimatedCost
│  ├─ safeUpperBoundCost
│  ├─ inputEstimatedTokens
│  ├─ inputSafeUpperBoundTokens
│  ├─ reservedOutputTokens
│  ├─ currency
│  ├─ canonicalModelId
│  ├─ pricingPolicyId
│  ├─ catalogVersion
│  └─ estimatorId / estimatorVersion
└─ Unavailable
   └─ bounded reason

CostBound 또는 Bounded 결과는 immutable value object다. estimatedCost는 관찰/표시용이며 #36의 reservation은 반드시 safeUpperBoundCost를 사용한다.

계산 전제

숫자 비용 상한은 다음 조건을 모두 만족할 때만 생성한다.

  1. input result가 정상 계산 상태다.
  2. scope가 REQUEST다.
  3. estimator/tokenizer compatibility가 model definition과 일치한다.
  4. reservedOutputTokens >= 0이고 [Core] TokenBudget.check()와 reserved output token 지원 #33 context admission과 일관된다.
  5. model이 참조하는 pricingPolicyIdcatalogVersion의 immutable snapshot을 찾을 수 있다.
  6. pricing policy가 해당 요청에 대해 유한한 보수적 상한을 제공할 수 있다.

조건을 만족하지 못하면 0원이나 임의의 fallback 단가를 반환하지 않고 typed Unavailable/INDETERMINATE 결과를 반환한다. budget enforcement 경계의 기본 정책은 fail-closed다.

TEXT_ONLY 값에 누락된 framing/headroom을 명시적으로 더해 REQUEST 결과를 만든 경우에만 사용할 수 있다. 단순히 scope enum을 변경해 비용 상한을 생성하는 것은 금지한다.

보수적 가격 계산 규칙

  • input은 inputSafeUpperBoundTokens를 사용한다.
  • output은 최대 생성 가능량인 reservedOutputTokens를 사용한다.
  • cache read/create, reasoning 등 호출 전에 breakdown을 확정할 수 없는 항목은 PricingPolicy가 가능한 적용 경로 중 최대 비용이 되는 유한 단가/조합을 선택한다.
  • 포함 관계인 cache/reasoning token을 total에 다시 더하지 않는다. 정책은 같은 token을 이중 과금하지 않아야 한다.
  • 할인만 있는 cache read를 발생한다고 가정해 상한을 낮추지 않는다.
  • reasoning이 일반 output 단가에 포함되는지 별도 단가인지 여부는 pricing policy가 명시적으로 소유한다.
  • 정책상 유한 상한을 계산할 수 없으면 Unavailable(UNBOUNDED_PRICING)로 반환한다.
  • safeUpperBoundCost >= estimatedCost >= 0
  • 모든 금액은 같은 currency이며 내부 계산에서 반올림하지 않는다.
  • 표시/청구 경계 전까지 [ADR] TokenUsage와 Money 불변식 및 정밀도 정책 정의 #24/#26의 BigDecimal 정밀도와 반올림 정책을 따른다.

가격 식별과 재현성

Bounded 결과에는 최소한 다음 식별자를 보존한다.

  • canonical model id
  • currency
  • pricingPolicyId
  • catalogVersion
  • estimatorId/version
  • 계산에 사용한 input estimate/safe bound와 reserved output

alias 입력도 먼저 canonical model로 해석한다. 계산 도중 catalog를 재조회해 다른 버전을 혼합하지 않고 하나의 immutable snapshot만 사용한다.

가격 미등록과 0원 정책

  • 가격 미등록: Unavailable(PRICING_NOT_FOUND), budget 경계 fail-closed
  • 필수 price component 누락: Unavailable(INCOMPLETE_PRICING)
  • 명시적인 무료 정책: 등록된 pricing policy가 모든 적용 단가를 정확히 0으로 선언한 경우에만 0원 Bound 생성
  • 통화 불일치: Unavailable(CURRENCY_MISMATCH), 환율 변환은 하지 않음

구현 범위

  • Core PreflightCostEstimator와 immutable/sealed result
  • 보수적 upper-bound 계산을 소유하는 pricing policy API
  • bounded unavailable reason
  • ModelRegistry/PricingRegistry immutable snapshot 연결
  • Money/Cost 기반 exact arithmetic
  • #36이 소비할 safeUpperBoundCost projection
  • 계산 근거를 로그/이벤트에 전달할 식별 metadata
  • Spring AI 타입 없는 public API

테스트 시나리오

  • REQUEST exact/heuristic 입력으로 estimatedCost와 safeUpperBoundCost를 계산한다.
  • safeUpperBoundCost >= estimatedCost >= 0 불변식을 검증한다.
  • input safe bound와 reserved output을 각각 올리면 상한이 감소하지 않는다.
  • cache/reasoning 가능 경로 중 보수적인 정책을 선택하고 token을 이중 합산하지 않는다.
  • TEXT_ONLY, unavailable count, tokenizer mismatch를 숫자 비용으로 변환하지 않는다.
  • 미등록/불완전/unbounded pricing은 typed unavailable이며 0원이 아니다.
  • 명시적 무료 정책만 정확한 0원 Bound를 반환한다.
  • alias와 canonical id가 같은 pricingPolicyId/catalogVersion을 사용한다.
  • 다른 currency를 합산하거나 변환하지 않는다.
  • 매우 큰 token 수에서도 long 합산 overflow를 피하고 BigDecimal 정밀도를 보존한다.
  • 요청마다 반올림하지 않는다.
  • 결과가 TokenUsage 타입을 재사용하지 않는다.

Acceptance criteria

  • token 상한을 금액 상한으로 바꾸는 단일 Core 계약이 추가된다.
  • Bounded와 Unavailable이 모순 없이 구분된다.
  • 비용 Bound는 REQUEST scope와 tokenizer compatibility가 확인된 경우에만 생성된다.
  • estimatedCost와 safeUpperBoundCost가 분리되고 reservation은 후자를 사용한다.
  • currency, pricingPolicyId, catalogVersion과 계산 token 근거가 결과에 포함된다.
  • 가격 미등록/불완전/무한 상한을 fail-closed로 처리한다.
  • 명시적 0원과 가격 미등록을 구분한다.
  • actual TokenUsage를 preflight 결과로 재사용하지 않는다.
  • Core 외부 의존성을 추가하지 않는다.

제외 범위

의존관계와 순서

선행:

이 이슈 완료 후 진행:

Source

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmvpTokenPilot 0.1.0 MVP scope

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions