Skip to content

[Core] framework-independent TokenEstimator와 TokenCountResult API 추가 - #56

Merged
HuitaePark merged 6 commits into
tokenpliot:mainfrom
Rigu1:feat/framework-independent-TokenEstimator-and-TokenCountResult-API
Aug 9, 2026
Merged

[Core] framework-independent TokenEstimator와 TokenCountResult API 추가#56
HuitaePark merged 6 commits into
tokenpliot:mainfrom
Rigu1:feat/framework-independent-TokenEstimator-and-TokenCountResult-API

Conversation

@Rigu1

@Rigu1 Rigu1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

변경 사항

호출 전 token 계산 결과를 Spring AI와 분리된 Core 계약으로 정의했습니다.

  • TokenEstimator를 framework-independent public interface로 추가했습니다.
  • TokenCountResult가 다음 상태를 상호 배타적으로 표현하도록 구현했습니다.
    • Counted(EXACT)
    • Counted(HEURISTIC)
    • Unavailable(reason)
  • TokenCountScopeTEXT_ONLYREQUEST를 구분했습니다.
  • TokenCountAccuracyEXACTHEURISTIC을 구분했습니다.
  • TokenEstimatorDescriptor에 estimator ID와 version을 보존합니다.
  • TokenizationBasis로 tokenizer compatibility 기준을 표현합니다.
  • 제한된 TokenCountUnavailableReason을 추가했습니다.
  • 상태 확인과 후속 정책 사용을 위한 public projection을 제공합니다.
  • plain-Java consumer에서 신규 API가 Spring 없이 동작하는지 검증했습니다.
  • 결과 계약과 후속 producer 책임을 행위 중심 문서로 정리했습니다.

배경

호출 후 provider 사용량인 TokenUsage와 호출 전 token 계산 결과는 의미와 생명주기가 다릅니다.

단순 token 숫자만 반환하면 다음 내용을 확인할 수 없습니다.

  • 문자열 내용만 계산했는지
  • role, tool schema, media metadata와 provider framing까지 포함했는지
  • 어떤 estimator와 tokenizer 기준으로 계산했는지
  • 정확한 값인지 보수적인 추정값인지

특히 TEXT_ONLY 결과가 전체 요청의 admission 근거로 사용되면 실제 request token을 과소평가할 수 있습니다.

이번 변경은 token 숫자와 계산 기준을 하나의 immutable result에 보존하고, 모순된 상태가 생성되지 않도록 Core 경계를 정의합니다.

결과 상태별 동작

EXACT

  • 계산값과 안전 상한이 동일해야 합니다.
  • exact 여부는 accuracy == EXACT에서 파생합니다.
  • 별도의 boolean 상태로 저장하지 않습니다.

HEURISTIC

  • 계산값과 admission에 사용할 보수적 상한을 별도로 제공합니다.
  • 안전 상한이 계산값보다 작은 결과는 생성할 수 없습니다.

UNAVAILABLE

  • 계산 불가 이유를 제한된 reason으로 제공합니다.
  • token 숫자나 안전 상한을 제공하지 않습니다.
  • 0 또는 음수를 unavailable sentinel로 사용하지 않습니다.
  • 정상적인 0-token 계산 결과와 구분됩니다.

Scope

TEXT_ONLY

전달된 문자열 내용만 계산한 결과입니다.

다음 요소를 포함했다고 간주하지 않습니다.

  • system/developer role
  • tool schema
  • image/audio metadata
  • structured-output schema
  • provider framing

따라서 TEXT_ONLY 결과만으로 전체 요청이 context window에 들어간다고 판단할 수 없습니다.

REQUEST

실제 전송 요청의 tokenizable content와 필요한 framing/headroom을 계산한 결과입니다.

기존 TEXT_ONLY 결과의 scope만 REQUEST로 변경하는 API는 제공하지 않습니다. 다른 계층이 누락된 구성요소를 계산했다면 새로운 REQUEST 결과를 생성해야 합니다.

범위 메모

이번 PR은 token 계산 결과의 계약과 불변식만 제공합니다.

TokenEstimator 구현체가 아직 없으므로 null 문자열과 빈 문자열의 실제 처리 행위는 #31의 producer 테스트에서 고정합니다.

  • null 문자열은 거부
  • 빈 문자열은 정상적인 0-token counted 결과로 처리
  • UTF-8 문자열 계산 결과에는 TEXT_ONLY 지정

REQUEST scope의 정당성은 실제 Spring AI 요청 구성요소와 framing을 계산하는 #39에서 검증합니다.

이후 과제

체크리스트

필수 테스트

  • EXACT 결과는 token과 safe upper bound가 같다.
  • HEURISTIC 결과는 safe upper bound가 estimate 이상이다.
  • EXACT, HEURISTIC, UNAVAILABLE 상태가 상호 배타적이다.
  • unavailable과 정상적인 0-token 결과를 구분한다.
  • 음수 token과 estimate보다 작은 bound를 거부한다.
  • null 또는 blank estimator descriptor 값을 거부한다.
  • TEXT_ONLY와 REQUEST를 구분한다.
  • 기존 결과의 scope를 변경하는 API가 없다.
  • estimator ID/version과 tokenization basis가 결과에 보존된다.
  • Spring AI가 없는 plain-Java consumer에서 API를 사용할 수 있다.

Acceptance Criteria

  • framework-independent TokenEstimator 계약을 제공한다.
  • exact, heuristic, unavailable을 모순 없이 표현한다.
  • TEXT_ONLY와 REQUEST의 의미를 Javadoc과 테스트에 고정한다.
  • estimator ID/version과 tokenizer compatibility 기준을 포함한다.
  • 계산값과 safe upper bound를 별도 값으로 제공한다.
  • TokenUsage를 preflight 결과로 재사용하지 않는다.
  • Core runtime dependency를 추가하지 않는다.

검증

  • 전체 Gradle check 통과
  • compatibility matrix 검증 통과
  • plain-Java Java 25 consumer 검증 통과
  • token-pilot-core Java 25 consumer OK

Closes #30

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • 새 기능
    • 토큰 수를 계산하고 결과를 제공하는 토큰 추정 기능을 추가했습니다.
    • 정확한 계산과 추정 계산을 구분하고, 토큰 수·안전 상한·계산 범위·기준 정보를 확인할 수 있습니다.
    • 빈 텍스트는 0토큰으로 처리하며, 계산할 수 없는 경우 별도 상태로 안내합니다.
    • 텍스트 기준과 요청 기준의 토큰 범위를 지원합니다.
  • 테스트
    • 토큰 결과 검증, 입력값 유효성, 0토큰 및 계산 불가 상태에 대한 검사를 추가했습니다.
  • 품질 개선
    • 핵심 실행 환경에서 허용되지 않는 런타임 구성 요소를 자동으로 점검합니다.

Walkthrough

TokenEstimator와 토큰 추정 결과 도메인 타입을 추가했다. TokenCountResult는 counted와 unavailable 상태, 정확도, 범위, 상한과 메타데이터를 검증한다. 생성된 consumer는 빈 문자열의 정확한 0-token 결과를 검사하고 금지된 런타임 의존성을 검사한다.

Changes

Token 추정 Core 계약

Layer / File(s) Summary
결과 메타데이터 계약
token-pilot-core/src/main/java/io/tokenpilot/core/domain/*
token-pilot-core/src/test/java/io/tokenpilot/core/domain/TokenEstimatorDescriptorTest.java, TokenizationBasisTest.java
TokenCountAccuracy, TokenCountScope, TokenCountUnavailableReason, TokenEstimatorDescriptor, TokenizationBasis를 추가했다. 각 식별자와 기준 값의 null·blank 입력을 검증한다.
TokenCountResult 상태와 불변식
token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java
token-pilot-core/src/test/java/io/tokenpilot/core/domain/TokenCountResultTest.java
counted와 unavailable 상태를 추가했다. 음수 값, EXACT 상한 불일치, HEURISTIC 상한 부족을 거부한다. 상태별 조회와 메타데이터 보존을 테스트한다.
Estimator 계약과 consumer 실행 검사
token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java, build.gradle
TokenEstimator.estimate(String) 계약을 추가했다. 생성된 consumer는 빈 문자열을 추정하고 counted·exact·0-token 결과를 검증한다. run 전에 금지된 런타임 의존성을 검사한다.

Suggested reviewers: huitaepark

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Linked Issues check ✅ Passed PR은 [#30]의 Core API, 상태 불변식, 메타데이터, scope 의미, Spring-free 사용 및 runtime 의존성 격리 요구를 충족합니다.
Out of Scope Changes check ✅ Passed 변경 사항은 [#30]의 계약 정의와 검증 범위에 포함되며, estimator 구현이나 Spring AI framing 같은 제외 범위를 추가하지 않습니다.
Title check ✅ Passed 제목은 framework-independent TokenEstimator 및 TokenCountResult Core API 추가라는 변경의 주요 내용을 명확하고 간결하게 설명합니다.
Description check ✅ Passed 설명은 추가된 Core API, 상태 불변식, 범위, 테스트 및 Spring-free 검증을 변경 사항과 직접 연결해 설명합니다.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

Caution

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

⚠️ Outside diff range comments (1)
build.gradle (1)

622-654: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

게시된 core 런타임 의존성을 별도로 검증하십시오.

CoreConsumer는 core artifact만 직접 선언합니다. Gradle은 core POM의 전이 런타임 의존성을 runtimeClasspath에 자동으로 포함합니다. 따라서 이 consumer가 컴파일되고 실행되어도 Spring AI, Spring Boot, Micrometer, Reactor가 core artifact를 통해 전이되지 않았다는 보장은 없습니다.

생성된 consumer build에서 runtimeClasspath의 해석된 component를 검사하고, 금지된 group 또는 module이 있으면 실패시키십시오. 이 검사를 run 또는 verifyCoreConsumer의 의존성으로 연결하십시오.

As per coding guidelines, "Keep token-pilot-core independently consumable without Spring AI, Spring Boot, Micrometer, or Reactor runtime dependencies, while preserving separate publication of existing modules."

🤖 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 `@build.gradle` around lines 622 - 654, Add a resolved runtimeClasspath
component check to the generated CoreConsumer build, rejecting any Spring AI,
Spring Boot, Micrometer, or Reactor group/module and failing with a clear
message. Wire this validation into run or verifyCoreConsumer so it executes
alongside the existing CoreConsumer verification, while keeping token-pilot-core
independently consumable and existing module publication unchanged.

Source: Coding guidelines

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

Outside diff comments:
In `@build.gradle`:
- Around line 622-654: Add a resolved runtimeClasspath component check to the
generated CoreConsumer build, rejecting any Spring AI, Spring Boot, Micrometer,
or Reactor group/module and failing with a clear message. Wire this validation
into run or verifyCoreConsumer so it executes alongside the existing
CoreConsumer verification, while keeping token-pilot-core independently
consumable and existing module publication unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 73934c61-4aca-4005-aee0-f7d9eb586b6c

📥 Commits

Reviewing files that changed from the base of the PR and between 100f5ab and 5cb20d6.

📒 Files selected for processing (11)
  • build.gradle
  • token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountAccuracy.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountScope.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountUnavailableReason.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenEstimatorDescriptor.java
  • token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenizationBasis.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/TokenCountResultTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/TokenEstimatorDescriptorTest.java
  • token-pilot-core/src/test/java/io/tokenpilot/core/domain/TokenizationBasisTest.java

@coderabbitai
coderabbitai Bot requested a review from HuitaePark August 7, 2026 13: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.

Caution

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

⚠️ Outside diff range comments (1)
build.gradle (1)

686-690: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

빈 문자열 입력을 실제로 검증하도록 수정해 주세요.

현재 TokenEstimator estimator = text -> expected;text를 무시합니다. 따라서 estimate("")를 다른 문자열로 변경해도 검사가 통과합니다. text.isEmpty()를 확인하는 테스트용 estimator를 사용해 빈 문자열 계약을 검증해 주세요.

수정 예시
-        TokenEstimator estimator = text -> expected;
+        TokenEstimator estimator = text -> {
+            if (!text.isEmpty()) {
+                throw new IllegalStateException(
+                    "Token Pilot estimator consumer expected empty input"
+                );
+            }
+            return expected;
+        };
🤖 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 `@build.gradle` around lines 686 - 690, Update the test estimator around
TokenEstimator and its estimate call so it validates the input is empty instead
of always returning expected regardless of text. Make the estimator return the
expected result only for an empty string, while preserving the existing
TokenCountResult assertions that verify the empty-input contract.
🤖 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.

Outside diff comments:
In `@build.gradle`:
- Around line 686-690: Update the test estimator around TokenEstimator and its
estimate call so it validates the input is empty instead of always returning
expected regardless of text. Make the estimator return the expected result only
for an empty string, while preserving the existing TokenCountResult assertions
that verify the empty-input contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e59afcee-8d32-49ab-8ed9-8a7514fe1857

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb20d6 and 3c91922.

📒 Files selected for processing (1)
  • build.gradle

@Rigu1

Rigu1 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

빈 문자열 입력을 실제로 검증하도록 수정해 주세요.

현재 TokenEstimator estimator = text -> expected;text를 무시합니다. 따라서 estimate("")를 다른 문자열로 변경해도 검사가 통과합니다. text.isEmpty()를 확인하는 테스트용 estimator를 사용해 빈 문자열 계약을 검증해 주세요.

제안된 lambda 검사는 빈 문자열 계약을 실제로 검증하는 역할이 아니며 #31 에서 검증하는 것이 맞다고 판단하여 반영하지 않았습니다.

@HuitaePark

Copy link
Copy Markdown
Member

검토 결과 실제 TokenEstimator/TokenCountResult 계약과 불변식은 #30 요구사항을 충족하고, 빌드 체크도 통과했습니다. 따라서 기능상 병합 가능한 것으로 판단합니다. 다만 생성된 core consumer의 테스트 estimator가 전달받은 문자열을 무시하고 항상 같은 결과를 반환하므로, 빈 문자열 계약을 직접 검증하지는 못합니다. 병합 후 text.isEmpty()를 확인하는 테스트로 보강해 주세요. AGENTS.md 갱신은 문서 후속 커밋으로 처리하면 됩니다.

@HuitaePark
HuitaePark merged commit 908fc44 into tokenpliot:main Aug 9, 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.

[Core] framework-independent TokenEstimator와 TokenCountResult API 추가

2 participants