From 386c3ae04fb572e52e128eb154adc4ff7236eed4 Mon Sep 17 00:00:00 2001 From: HuitaePark Date: Thu, 27 Aug 2026 10:21:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20OpenAI=20smoke=EC=99=80=20=EC=98=81?= =?UTF-8?q?=EB=AC=B8=20=EC=BD=94=EB=93=9C=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 16 +- README.md | 8 + docs/CONFIGURATION.md | 24 ++ docs/QUICKSTART.md | 40 ++- docs/SAMPLE_RUNBOOK.md | 54 +++- .../LedgerChatClientBuilderCustomizer.java | 2 +- .../autoconfigure/PricingPlanProperties.java | 8 +- .../TokenPilotAutoConfiguration.java | 2 +- .../TokenPilotBudgetAutoConfiguration.java | 2 +- ...enPilotBudgetMetricsAutoConfiguration.java | 2 +- .../TokenPilotBudgetPolicyFactory.java | 6 +- ...okenPilotCoreMetricsAutoConfiguration.java | 2 +- ...kenPilotNotificationAutoConfiguration.java | 2 +- ...tNotificationMetricsAutoConfiguration.java | 2 +- .../autoconfigure/TokenPilotProperties.java | 6 +- .../TokenPilotSpringAiAutoConfiguration.java | 2 +- .../TokenPilotAutoConfigurationTest.java | 2 +- .../budget/AccountingTransitionStatus.java | 25 +- .../tokenpilot/budget/ActualUsageCommand.java | 2 +- .../budget/AtomicBudgetStateStore.java | 9 +- .../io/tokenpilot/budget/BudgetDecision.java | 33 +-- .../io/tokenpilot/budget/BudgetEvaluator.java | 23 +- .../java/io/tokenpilot/budget/BudgetKey.java | 10 +- .../io/tokenpilot/budget/BudgetPolicy.java | 16 +- .../tokenpilot/budget/BudgetReservation.java | 18 +- .../budget/BudgetReservationRequest.java | 21 +- .../budget/BudgetReservationResult.java | 2 +- .../io/tokenpilot/budget/BudgetSnapshot.java | 6 +- .../io/tokenpilot/budget/BudgetState.java | 10 +- .../tokenpilot/budget/BudgetStateStore.java | 47 +-- .../io/tokenpilot/budget/BudgetThreshold.java | 16 +- .../io/tokenpilot/budget/BudgetWindow.java | 4 +- .../io/tokenpilot/budget/IdempotencyKey.java | 2 +- .../budget/ReservationAccounting.java | 80 +++--- .../budget/ReservationAccountingEvent.java | 2 +- .../budget/ReservationAccountingListener.java | 25 +- ...rvationAccountingListenerErrorHandler.java | 2 +- ...rvationAccountingListenerFailureEvent.java | 6 +- .../ReservationAccountingListenerPhase.java | 2 +- .../ReservationAccountingListenerType.java | 2 +- .../budget/ReservationAccountingReason.java | 2 +- .../budget/ReservationActualTokens.java | 2 +- .../io/tokenpilot/budget/ReservationId.java | 2 +- .../budget/ReservationReconciliation.java | 25 +- ...eservationReconciliationRequiredEvent.java | 4 +- .../tokenpilot/budget/ReservationState.java | 18 +- .../budget/ReservationStateMachine.java | 16 +- .../tokenpilot/budget/ReservationStatus.java | 2 +- .../budget/ReservationTokenEstimate.java | 2 +- .../budget/ReservationTransition.java | 2 +- .../exception/BudgetExceededException.java | 7 +- .../internal/ActualUsageFingerprint.java | 2 +- .../budget/internal/AppliedCommit.java | 3 +- .../budget/internal/AppliedRelease.java | 2 +- .../budget/internal/CommitType.java | 2 +- .../internal/DefaultBudgetEvaluator.java | 8 +- .../internal/InMemoryBudgetStateStore.java | 10 +- .../internal/LedgerBudgetComponents.java | 14 +- .../budget/internal/ReleaseType.java | 2 +- .../internal/ReservationAccountingState.java | 2 +- .../BudgetReservationConcurrencyTest.java | 10 +- .../io/tokenpilot/core/CoreComponents.java | 2 +- .../io/tokenpilot/core/CostCalculator.java | 12 +- .../io/tokenpilot/core/LedgerListener.java | 8 +- .../io/tokenpilot/core/LedgerManager.java | 32 +-- .../io/tokenpilot/core/ModelRegistry.java | 17 +- .../core/PreflightCostEstimator.java | 39 +-- .../core/PreflightDecisionListener.java | 2 +- .../io/tokenpilot/core/PricingEvaluator.java | 16 +- .../core/PricingMissingListener.java | 2 +- .../io/tokenpilot/core/PricingProvider.java | 8 +- .../io/tokenpilot/core/PricingRegistry.java | 60 ++-- .../io/tokenpilot/core/RoundingPolicy.java | 3 +- .../java/io/tokenpilot/core/TokenBudget.java | 20 +- .../io/tokenpilot/core/TokenEstimator.java | 15 +- .../core/domain/AdmissionReason.java | 14 +- .../core/domain/AdmissionStatus.java | 8 +- .../tokenpilot/core/domain/BudgetResult.java | 11 +- .../java/io/tokenpilot/core/domain/Cost.java | 9 +- .../core/domain/CostRecordedEvent.java | 4 +- .../core/domain/ModelDefinition.java | 24 +- .../core/domain/PreflightCostResult.java | 63 ++-- .../PreflightCostUnavailableReason.java | 21 +- .../core/domain/PreflightDecisionEvent.java | 4 +- .../core/domain/PreflightPricingContext.java | 62 ++-- .../core/domain/PricingMissingEvent.java | 6 +- .../tokenpilot/core/domain/PricingPlan.java | 30 +- .../domain/PricingReconciliationResult.java | 2 +- .../core/domain/PricingSnapshot.java | 14 +- .../core/domain/TokenCountAccuracy.java | 6 +- .../core/domain/TokenCountResult.java | 78 ++--- .../core/domain/TokenCountScope.java | 15 +- .../domain/TokenCountUnavailableReason.java | 4 +- .../core/domain/TokenEstimatorDescriptor.java | 10 +- .../io/tokenpilot/core/domain/TokenType.java | 20 +- .../io/tokenpilot/core/domain/TokenUsage.java | 70 ++--- .../core/domain/TokenUsageDetails.java | 20 +- .../core/domain/TokenizationBasis.java | 9 +- .../tokenpilot/core/domain/UsageSource.java | 12 +- .../core/internal/DefaultCostCalculator.java | 8 +- .../core/internal/DefaultLedgerManager.java | 9 +- .../DefaultPreflightCostEstimator.java | 12 +- .../core/internal/DefaultTokenBudget.java | 2 +- .../internal/HeuristicTokenEstimator.java | 18 +- .../core/internal/InMemoryModelRegistry.java | 2 +- .../internal/InMemoryPricingRegistry.java | 2 +- .../core/internal/LedgerComponents.java | 30 +- .../tokenpilot/micrometer/MetricsOptions.java | 8 +- .../internal/BudgetMetricsPublisher.java | 2 +- .../internal/BudgetMicrometerComponents.java | 2 +- .../internal/CoreMetricsPublisher.java | 2 +- .../internal/LedgerMicrometerComponents.java | 2 +- .../internal/MicroCostMetricsPublisher.java | 6 +- .../NotificationMetricsPublisher.java | 2 +- .../NotificationMicrometerComponents.java | 2 +- .../MicroCostMetricsPublisherTest.java | 6 +- .../AtomicNotificationStateStore.java | 2 +- .../notification/BudgetNotificationError.java | 7 +- .../BudgetNotificationErrorHook.java | 2 +- .../notification/BudgetNotificationEvent.java | 38 +-- .../BudgetNotificationHandler.java | 4 +- .../BudgetNotificationLifecycleEvent.java | 7 +- .../BudgetNotificationLifecycleListener.java | 2 +- .../BudgetNotificationOutcome.java | 2 +- .../BudgetNotificationService.java | 38 +-- .../BudgetNotificationSource.java | 2 +- .../InMemoryNotificationStateStore.java | 12 +- .../notification/NotificationStateStore.java | 11 +- token-pilot-sample-app/DEMO_RUNBOOK.md | 54 +++- token-pilot-sample-app/DEMO_RUNBOOK_EN.md | 150 ++++++++++ token-pilot-sample-app/Dockerfile | 6 +- token-pilot-sample-app/build.gradle | 9 +- token-pilot-sample-app/docker-compose.yml | 6 +- .../provisioning/datasources/datasource.yml | 4 +- token-pilot-sample-app/prometheus/Dockerfile | 4 +- .../prometheus/prometheus.yml | 4 +- .../io/tokenpilot/sample/DemoChatModel.java | 9 +- .../tokenpilot/sample/DemoConfiguration.java | 2 +- .../io/tokenpilot/sample/DemoController.java | 2 +- .../io/tokenpilot/sample/DemoRunReport.java | 2 +- .../tokenpilot/sample/DemoScenarioResult.java | 2 +- .../sample/DemoScenarioService.java | 7 +- .../sample/OpenAiSmokeController.java | 268 ++++++++++++++++++ .../tokenpilot/sample/OpenAiSmokeReport.java | 30 ++ .../resources/application-openai-smoke.yml | 56 ++++ .../src/main/resources/application.yml | 11 + .../sample/DemoScenarioE2ETest.java | 2 +- .../tokenpilot/sample/OpenAiSmokeE2ETest.java | 62 ++++ token-pilot-spring-ai/build.gradle | 2 +- .../io/tokenpilot/springai/LedgerAdvisor.java | 5 +- .../ReservedOutputTokensResolver.java | 5 +- .../tokenpilot/springai/UsageExtractor.java | 3 +- .../internal/DefaultLedgerAdvisor.java | 18 +- .../internal/DefaultUsageExtractor.java | 9 +- .../internal/IdempotencyKeyResolver.java | 2 +- .../internal/LedgerSpringAiComponents.java | 2 +- .../springai/internal/ModelResolver.java | 2 +- .../internal/RequestContextAccessor.java | 2 +- .../springai/internal/RequestMessageRole.java | 2 +- .../springai/internal/RequestPreflight.java | 2 +- .../internal/RequestScopeResolver.java | 2 +- .../springai/internal/RequestScopeResult.java | 2 +- .../internal/ReservedOutputResolver.java | 2 +- .../UnsupportedRequestScopeReason.java | 2 +- 164 files changed, 1631 insertions(+), 795 deletions(-) create mode 100644 token-pilot-sample-app/DEMO_RUNBOOK_EN.md create mode 100644 token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeController.java create mode 100644 token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeReport.java create mode 100644 token-pilot-sample-app/src/main/resources/application-openai-smoke.yml create mode 100644 token-pilot-sample-app/src/test/java/io/tokenpilot/sample/OpenAiSmokeE2ETest.java diff --git a/AGENTS.md b/AGENTS.md index 11addf9..2b29ab9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ Token Pilot의 제품 포지션은 framework-independent Java LLM control and ac | `token-pilot-notification` | Atomic accounting integration implemented | Commit, reconciliation-required, late reconciliation, and reservation BLOCK results produce process-local atomically deduplicated threshold events with isolated handlers, lifecycle observation, and a sanitized error hook; durable delivery remains | | `token-pilot-autoconfigure` | Basic implementation complete | Owner-specific optional bean graphs wire pricing, atomic budget/accounting, Spring AI, notification, and metrics while preserving budget-disabled ledger-only compatibility and `ChatClientBuilderCustomizer` | | `token-pilot-starter` | Basic implementation complete | Thin final user entrypoint that brings runtime modules together | -| `token-pilot-sample-app` | Basic E2E complete | Direct ledger metrics and fake Spring AI lifecycle E2E cover preflight BLOCK, reservation, reconciliation, request-scope rejection, Advisor ordering, listener isolation, and enforcement streaming rejection without an API key | +| `token-pilot-sample-app` | Basic E2E complete | Direct ledger metrics, deterministic fake-provider demo E2E, and an opt-in OpenAI provider smoke path cover the sample integration; live provider compatibility still requires an explicit API-key run | ## Current Work Focus @@ -306,6 +306,13 @@ sample app's `DEMO_RUNBOOK.md` documents the Gradle, Prometheus/Grafana, and sce verification flow. The provisioned dashboard uses Token Pilot-owned `tokenpilot_*` meters and deliberately does not use legacy `ai_token_*` meters. +OpenAI smoke profile endpoints (run with `--spring.profiles.active=openai-smoke`): + +- `GET /test/token-pilot/openai-smoke`: performs one real OpenAI Chat Completions call, + returns normalized usage and accounting evidence, and requires `OPENAI_API_KEY`. +- `OpenAiSmokeE2ETest`: is skipped by default and only runs when both + `RUN_OPENAI_SMOKE=true` and `OPENAI_API_KEY` are present. + Test-only E2E endpoint: - `GET /test/token-pilot/chat`: exercises the Spring AI `ChatClient` advisor path with a fake/mock provider or documented real provider setup. @@ -369,7 +376,7 @@ The active checklist is in `docs/30_DAY_MVP_REPORT.md`; detailed long-term works - A response model that differs from the reservation pricing snapshot is not committed at the request model price; it remains pending with `PRICING_RECONCILIATION_REQUIRED` for later settlement. - Pricing-mismatch pending events preserve provider actual usage and response model metadata. Consumers may call the explicit `reconcileLateActual(command, responsePricingSnapshot)` API with an immutable response-model pricing snapshot; model, currency, rate, state, and duplicate-callback validation remain fail-closed. - The repository, README, JReleaser configuration, and every published module POM use the MIT License. `verifyPublicationMetadata` guards this release contract and ensures the sample app is not published. -- Sample app E2E uses a fake Spring AI `ChatModel`; real provider API behavior is not yet verified. +- Sample app's default/demo E2E uses a fake Spring AI `ChatModel`; the OpenAI smoke path is opt-in and is not a CI/live compatibility guarantee until the guarded test is run with a real key. - `token-pilot-spring-ai-starter` does not exist in the current build; never use it as an install instruction until implemented and published. - Maven Central release consumption must be re-verified for both core and starter paths before announcing `0.1.0`. - Preflight cost bounds must use one immutable pricing snapshot from calculation through reservation and reconciliation; resolving a mutable registry again by model/policy identifiers can mix prices from different requests. @@ -471,6 +478,11 @@ Stage and deploy a Central release: ## Update History +### 2026-08-27 + +- Added the sample app's application-owned OpenAI starter dependency, isolated `openai-smoke` profile, usage/accounting evidence endpoint, and key-gated live JUnit smoke test; default and demo profiles explicitly disable provider auto-configuration so deterministic tests remain unchanged. +- Documented the sequential demo → OpenAI smoke procedure, environment-only API key handling, model/pricing requirements, and Prometheus evidence. + ### 2026-08-26 - Added root publication aggregation and a release-candidate verification gate diff --git a/README.md b/README.md index 15ad431..c1f60ed 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ against the reservation-time pricing snapshot. - [Metrics reference](docs/METRICS.md) — Token Pilot-owned meters and legacy compatibility policy. - [Sample app runbook](docs/SAMPLE_RUNBOOK.md) — local app, Prometheus, Grafana, and troubleshooting commands. - [Deterministic demo runbook](token-pilot-sample-app/DEMO_RUNBOOK.md) — eight admission, reservation, idempotency, release, and reconciliation scenarios. +- [English demo runbook](token-pilot-sample-app/DEMO_RUNBOOK_EN.md) — the same demo and OpenAI smoke procedure in English. +- The same [sample app runbook](docs/SAMPLE_RUNBOOK.md) includes the opt-in OpenAI provider smoke path. - [Release procedure](docs/RELEASE.md) — staging, signing, external consumer, and Central Portal gates. - [30-day MVP cutline](docs/30_DAY_MVP_REPORT.md) and [post-MVP evolution plan](docs/EVOLUTION_PLAN.md). @@ -92,6 +94,12 @@ See [QUICKSTART.md](docs/QUICKSTART.md) for the ten-minute path and [SAMPLE_RUNBOOK.md](docs/SAMPLE_RUNBOOK.md) for endpoint and troubleshooting details. +The sample app's `openai-smoke` profile uses the application-selected OpenAI +starter and exposes `/test/token-pilot/openai-smoke`. It requires +`OPENAI_API_KEY`, makes a real provider call, and reports normalized usage plus +the resulting Token Pilot accounting state. The guarded live test is skipped +unless `RUN_OPENAI_SMOKE=true` is set. + ## Micrometer metrics When a `MeterRegistry` is available, the Spring Boot starter publishes diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 112051f..bdd1a78 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -52,6 +52,30 @@ is `TEXT_ONLY`; media, tool messages or schemas, structured-output augmentation, and enforcement-enabled streaming are rejected before reservation when they cannot be represented by the supported contract. +## Sample OpenAI smoke profile + +The repository sample app selects an actual provider only when the +`openai-smoke` profile is active. The provider dependency is application-owned: + +```gradle +implementation 'cloud.token-pilot:token-pilot-starter:0.0.1-SNAPSHOT' +implementation 'org.springframework.ai:spring-ai-starter-model-openai:2.0.0' +``` + +The profile reads `OPENAI_API_KEY` and `OPENAI_MODEL` from the environment and +uses a versioned catalog entry by default. Its Token Pilot pricing values are +example rates per 1,000 tokens and should be replaced with the rates for the +selected provider model before using the result as a financial record. + +The sample endpoint is: + +```text +GET /test/token-pilot/openai-smoke +``` + +See [`docs/SAMPLE_RUNBOOK.md`](SAMPLE_RUNBOOK.md) for the demo-to-smoke +sequence and the guarded live test command. + ## Metrics | Property | Default | Meaning | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 96228dd..a1b05e3 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -132,7 +132,45 @@ curl -s http://localhost:8080/test/token-pilot/demo/reconciliation-failure | jq The complete sample procedure is in [`token-pilot-sample-app/DEMO_RUNBOOK.md`](../token-pilot-sample-app/DEMO_RUNBOOK.md). -## 3. Prometheus and Grafana +## 3. OpenAI provider smoke + +The sample app also includes an opt-in path that uses the real OpenAI provider. +Stop the `demo` profile first, then set the key in the environment. The call +can incur provider charges, so keep the prompt short and never commit the key. + +```bash +export OPENAI_API_KEY='your OpenAI API key' +export OPENAI_MODEL='gpt-4o-mini-2024-07-18' + +./gradlew --no-daemon :token-pilot-sample-app:bootRun \ + --args='--spring.profiles.active=openai-smoke' +``` + +In another terminal: + +```bash +curl -sS --get http://localhost:8080/test/token-pilot/openai-smoke \ + --data-urlencode 'prompt=Reply with one short sentence confirming the provider is reachable.' \ + | jq +``` + +The expected evidence is `status=PASS`, a reported usage source, and +`accountingState=COMMITTED`. The default model is the versioned model already +known by the sample catalog. If `OPENAI_MODEL` is changed, its catalog entry +and pricing plan must also match the response model. + +The live test is guarded and skipped by the normal test suite: + +```bash +RUN_OPENAI_SMOKE=true \ + ./gradlew --no-daemon :token-pilot-sample-app:test \ + --tests io.tokenpilot.sample.OpenAiSmokeE2ETest +``` + +Run the demo first, stop it, then start `openai-smoke`; both profiles use port +`8080` and must not run concurrently. + +## 4. Prometheus and Grafana Keep the sample app running and start the monitoring services: diff --git a/docs/SAMPLE_RUNBOOK.md b/docs/SAMPLE_RUNBOOK.md index b757034..56ecd80 100644 --- a/docs/SAMPLE_RUNBOOK.md +++ b/docs/SAMPLE_RUNBOOK.md @@ -114,7 +114,59 @@ curl -s http://localhost:8080/test/token-pilot/demo/reconciliation-unknown | jq ``` The endpoint list and the effective profile configuration are also maintained -in [`token-pilot-sample-app/DEMO_RUNBOOK.md`](../token-pilot-sample-app/DEMO_RUNBOOK.md). +in the [Korean demo runbook](../token-pilot-sample-app/DEMO_RUNBOOK.md) and the +[English demo runbook](../token-pilot-sample-app/DEMO_RUNBOOK_EN.md). + +## OpenAI provider smoke profile + +Stop the demo app before starting this profile. The sample app selects the +OpenAI provider only for `openai-smoke`; the default and `demo` profiles keep +provider auto-configuration disabled so fake-provider tests remain deterministic. + +Set the API key in the environment. Do not put it in a source file or commit it. +The default model is the versioned catalog entry +`gpt-4o-mini-2024-07-18`; each request is a real provider call and may incur +provider charges. + +```bash +export OPENAI_API_KEY='your OpenAI API key' +export OPENAI_MODEL='gpt-4o-mini-2024-07-18' + +./gradlew --no-daemon :token-pilot-sample-app:bootRun \ + --args='--spring.profiles.active=openai-smoke' +``` + +In another terminal, run one short request: + +```bash +curl -sS --get http://localhost:8080/test/token-pilot/openai-smoke \ + --data-urlencode 'prompt=Reply with one short sentence confirming the provider is reachable.' \ + | jq +``` + +The response is a pass when it reports `status=PASS`, a non-`UNAVAILABLE` +`usageSource`, and `accountingState=COMMITTED`. It also returns normalized +input/output/total tokens, the newly applied cost metric delta, and the budget +snapshot. Inspect the rendered meters with: + +```bash +curl -sS http://localhost:8080/actuator/prometheus \ + | rg '^tokenpilot_(preflight|budget|cost|reconciliation)' +``` + +The opt-in JUnit smoke test uses the same endpoint and is guarded by both +`RUN_OPENAI_SMOKE=true` and `OPENAI_API_KEY`: + +```bash +RUN_OPENAI_SMOKE=true \ + ./gradlew --no-daemon :token-pilot-sample-app:test \ + --tests io.tokenpilot.sample.OpenAiSmokeE2ETest +``` + +To run both paths, execute `/test/token-pilot/demo/run` while the `demo` profile +is active, stop that process, then restart with `openai-smoke` and call the +smoke endpoint. They intentionally share port `8080` and should not run at the +same time. ## Prometheus and Grafana diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/LedgerChatClientBuilderCustomizer.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/LedgerChatClientBuilderCustomizer.java index 5b17ffa..35ed942 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/LedgerChatClientBuilderCustomizer.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/LedgerChatClientBuilderCustomizer.java @@ -5,7 +5,7 @@ import org.springframework.ai.chat.client.ChatClientBuilderCustomizer; /** - * ChatClient.Builder에 LedgerAdvisor를 자동으로 주입하는 커스터마이저. + * Customizer that automatically injects LedgerAdvisor into ChatClient.Builder. */ public class LedgerChatClientBuilderCustomizer implements ChatClientBuilderCustomizer { diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/PricingPlanProperties.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/PricingPlanProperties.java index a7cee32..58e5ee5 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/PricingPlanProperties.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/PricingPlanProperties.java @@ -9,11 +9,11 @@ import java.util.Objects; /** - * 개별 AI 모델에 대한 가격 설정 정보. + * Pricing configuration for an individual AI model. * - * @param modelId AI 모델 식별자 (예: gpt-4o, Claude-3-5-sonnet). 이 값은 ChatResponse의 model_id와 매칭되어야 합니다. - * @param rates 토큰 타입별 1,000(1K) 토큰당 가격 설정. (PROMPT, COMPLETION 등) - * @param currency 가격 정산에 사용할 통화 단위 (기본값: USD). + * @param modelId AI model identifier, such as gpt-4o or Claude-3-5-sonnet. This value must match the ChatResponse model_id. + * @param rates per-1,000 (1K) token price by token type, such as PROMPT or COMPLETION + * @param currency currency used for cost settlement, defaulting to USD */ public record PricingPlanProperties( String modelId, diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java index 6225c79..be03728 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfiguration.java @@ -20,7 +20,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -/** Token Pilot의 framework-independent core 자동 설정입니다. */ +/** Auto-configuration for the Token Pilot framework-independent core. */ @AutoConfiguration @ConditionalOnProperty( prefix = "token-pilot", diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetAutoConfiguration.java index 3b914d7..80ff896 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetAutoConfiguration.java @@ -15,7 +15,7 @@ import java.time.Clock; -/** Optional budget module의 bean graph를 격리하는 자동 설정입니다. */ +/** Auto-configuration isolating the optional budget module bean graph. */ @AutoConfiguration(after = { TokenPilotAutoConfiguration.class, TokenPilotBudgetMetricsAutoConfiguration.class diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetMetricsAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetMetricsAutoConfiguration.java index 28a5726..3ceaecd 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetMetricsAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetMetricsAutoConfiguration.java @@ -11,7 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -/** Atomic reservation/accounting metrics의 조건부 자동 설정입니다. */ +/** Conditional auto-configuration for atomic reservation/accounting metrics. */ @AutoConfiguration(after = TokenPilotCoreMetricsAutoConfiguration.class) @ConditionalOnClass({ MeterRegistry.class, diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetPolicyFactory.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetPolicyFactory.java index 0e63186..09445fa 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetPolicyFactory.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotBudgetPolicyFactory.java @@ -8,10 +8,10 @@ import java.util.Objects; /** - * Token Pilot 설정을 optional budget 정책으로 변환하는 공개 진입점입니다. + * Public entry point for converting Token Pilot settings into an optional budget policy. * - *

budget 타입을 {@link TokenPilotProperties}에서 분리해 budget 모듈이 없는 - * core-only 자동 설정도 class loading에 실패하지 않도록 합니다.

+ *

Budget types are kept separate from {@link TokenPilotProperties} so + * core-only auto-configuration can load when the budget module is absent.

*/ public final class TokenPilotBudgetPolicyFactory { diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotCoreMetricsAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotCoreMetricsAutoConfiguration.java index 5d09d94..69933d4 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotCoreMetricsAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotCoreMetricsAutoConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -/** Core control events와 legacy ledger metrics의 조건부 자동 설정입니다. */ +/** Conditional auto-configuration for core control events and legacy ledger metrics. */ @AutoConfiguration(after = TokenPilotAutoConfiguration.class, afterName = { "org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration", "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration" diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationAutoConfiguration.java index 826ceaf..85a7f1b 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationAutoConfiguration.java @@ -16,7 +16,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -/** Optional notification module의 bean graph를 격리하는 자동 설정입니다. */ +/** Auto-configuration isolating the optional notification module bean graph. */ @AutoConfiguration(after = { TokenPilotAutoConfiguration.class, TokenPilotNotificationMetricsAutoConfiguration.class diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationMetricsAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationMetricsAutoConfiguration.java index 0b39766..b0011b9 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationMetricsAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotNotificationMetricsAutoConfiguration.java @@ -11,7 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -/** Budget notification lifecycle metrics의 조건부 자동 설정입니다. */ +/** Conditional auto-configuration for budget notification lifecycle metrics. */ @AutoConfiguration(after = TokenPilotCoreMetricsAutoConfiguration.class) @ConditionalOnClass({ MeterRegistry.class, diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotProperties.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotProperties.java index 0d512f9..a29d9cd 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotProperties.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotProperties.java @@ -10,7 +10,7 @@ import java.util.Set; /** - * Token Pilot의 설정을 담당하는 프로퍼티 클래스. + * Configuration properties for Token Pilot. */ @ConfigurationProperties(prefix = "token-pilot") public class TokenPilotProperties { @@ -186,7 +186,7 @@ public void setTargetTagKey(String targetTagKey) { } /** - * 대상 tag가 없을 때 사용할 명시적 fallback입니다. 미설정 시 평가는 fail-closed 됩니다. + * Explicit fallback used when the target tag is absent. Evaluation fails closed when it is unset. */ public String getFallbackTargetId() { return fallbackTargetId; @@ -205,7 +205,7 @@ public void setCurrency(String currency) { } /** - * 월별 budget window 경계를 계산하는 IANA ZoneId입니다. 기본값은 UTC입니다. + * IANA ZoneId used to calculate monthly budget-window boundaries. The default is UTC. */ public String getZoneId() { return zoneId; diff --git a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotSpringAiAutoConfiguration.java b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotSpringAiAutoConfiguration.java index 2f49df1..f077b3a 100644 --- a/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotSpringAiAutoConfiguration.java +++ b/token-pilot-autoconfigure/src/main/java/io/tokenpilot/autoconfigure/TokenPilotSpringAiAutoConfiguration.java @@ -25,7 +25,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -/** Optional Spring AI adapter의 bean graph를 격리하는 자동 설정입니다. */ +/** Auto-configuration isolating the optional Spring AI adapter bean graph. */ @AutoConfiguration(after = { TokenPilotAutoConfiguration.class, TokenPilotBudgetAutoConfiguration.class, diff --git a/token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java b/token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java index c3eb8f6..8a85f86 100644 --- a/token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java +++ b/token-pilot-autoconfigure/src/test/java/io/tokenpilot/autoconfigure/TokenPilotAutoConfigurationTest.java @@ -1104,7 +1104,7 @@ int evaluations() { } } - // 테스트용 no-op handler - 실제 알림 전송 없이 빈 등록 여부만 검증 + // Test-only no-op handler; verifies bean registration without sending notifications. @Configuration(proxyBeanMethods = false) static class FakeBudgetNotificationHandlerConfiguration { @Bean diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/AccountingTransitionStatus.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/AccountingTransitionStatus.java index 00df53c..8a0de70 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/AccountingTransitionStatus.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/AccountingTransitionStatus.java @@ -1,35 +1,36 @@ package io.tokenpilot.budget; /** - * 예약 생성 이후 회계 상태 변경 명령의 결과입니다. + * Result of an accounting state-change command after reservation creation. * - *

{@link #APPLIED}만 새로운 회계 변경이 적용되었음을 뜻합니다. 나머지 결과는 기존 상태와 - * 금액을 바꾸지 않았음을 뜻하므로 호출자는 예외 메시지나 저장소 내부 구현에 의존하지 않고 - * 재시도, 충돌 처리, 입력 보정을 결정할 수 있습니다.

+ *

Only {@link #APPLIED} means that a new accounting change was applied. All + * other results leave the existing state and amounts unchanged, allowing callers + * to decide on retries, conflict handling, or input correction without depending + * on exception messages or store internals.

*/ public enum AccountingTransitionStatus { - /** 요청한 회계 변경이 새로 적용되었습니다. */ + /** The requested accounting change was newly applied. */ APPLIED, - /** 같은 회계 명령과 값이 이미 적용되어 기존 결과가 재사용되었습니다. */ + /** The same accounting command and values were already applied, so the existing result was reused. */ REUSED, - /** 기존에 적용된 종료 명령 또는 값이 새 요청과 충돌합니다. */ + /** An already applied terminal command or value conflicts with the new request. */ CONFLICT, - /** 대상 예약을 찾을 수 없습니다. */ + /** The target reservation was not found. */ NOT_FOUND, - /** 명령 금액의 통화가 예약 통화와 다릅니다. */ + /** The command amount uses a different currency from the reservation. */ CURRENCY_MISMATCH, - /** 명령 인자가 계약을 만족하지 않습니다. */ + /** The command arguments do not satisfy the contract. */ INVALID_ARGUMENT, - /** 현재 예약 상태에서는 요청한 전이가 허용되지 않습니다. */ + /** The requested transition is not allowed from the current reservation state. */ NOT_ALLOWED; - /** 새로운 회계 변경이 적용되었는지 반환합니다. */ + /** Returns whether a new accounting change was applied. */ public boolean isApplied() { return this == APPLIED; } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ActualUsageCommand.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ActualUsageCommand.java index c7bb159..804fe30 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ActualUsageCommand.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ActualUsageCommand.java @@ -6,7 +6,7 @@ import java.util.Objects; /** - * provider가 보고한 actual usage를 하나의 예약에 정산하기 위한 명령입니다. + * Command for settling provider-reported actual usage against one reservation. */ public record ActualUsageCommand( String requestId, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/AtomicBudgetStateStore.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/AtomicBudgetStateStore.java index f8edc5a..286c80e 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/AtomicBudgetStateStore.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/AtomicBudgetStateStore.java @@ -1,11 +1,12 @@ package io.tokenpilot.budget; /** - * 예약 승인과 후속 회계 전이를 같은 원자적 상태에서 처리하는 budget store입니다. + * Budget store that handles reservation admission and subsequent accounting + * transitions in the same atomic state. * - *

Spring 같은 DI container는 이 합성 계약을 단일 bean으로 노출해 - * {@link BudgetStateStore}와 {@link ReservationAccounting}이 서로 다른 저장소로 - * 연결되는 것을 방지할 수 있습니다.

+ *

A dependency-injection container such as Spring can expose this composite + * contract as one bean to prevent {@link BudgetStateStore} and + * {@link ReservationAccounting} from being connected to different stores.

*/ public interface AtomicBudgetStateStore extends BudgetStateStore, ReservationAccounting { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java index ab113a4..a583f6c 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetDecision.java @@ -5,21 +5,22 @@ import java.util.Objects; /** - * 부수 효과 없는 예산 평가 결과입니다. + * Side-effect-free budget evaluation result. * - * @param key 평가 시점에 확정된 예산 bucket 식별자 - * @param evaluationType 후보 비용 포함 여부 - * @param state ALLOW / WARN / BLOCK / CURRENCY_MISMATCH 상태 - * @param threshold 예상 사용량이 도달한 예산 임계치 - * @param reason 상태 설명 - * @param committedUsage 현재 확정된 누적 사용량 - * @param projectedUsage 후보 비용을 포함한 예상 사용량. 상태 조회이거나 통화가 일치하지 않으면 - * {@code committedUsage}와 같습니다. - * @param limit 통화를 포함한 총 예산 + * @param key budget bucket identifier fixed at evaluation time + * @param evaluationType whether candidate cost is included + * @param state ALLOW / WARN / BLOCK / CURRENCY_MISMATCH state + * @param threshold budget threshold reached by projected usage + * @param reason state explanation + * @param committedUsage currently committed cumulative usage + * @param projectedUsage projected usage including candidate cost. It equals + * {@code committedUsage} for status queries or currency mismatches. + * @param limit total budget including its currency * - *

Migration note: {@link EvaluationType#STATUS} 결과는 현재 상태 조회용이며 - * provider 호출 허가의 근거가 아닙니다. 기존의 모호한 {@code currentUsage} 대신 확정 - * 사용량은 {@code committedUsage}, 후보 포함 사용량은 {@code projectedUsage}를 사용합니다. + *

Migration note: {@link EvaluationType#STATUS} results are + * for current-state queries and are not grounds for authorizing provider calls. + * Use {@code committedUsage} for committed usage and {@code projectedUsage} for + * usage including a candidate instead of the former ambiguous {@code currentUsage}. */ public record BudgetDecision( BudgetKey key, @@ -57,7 +58,7 @@ public record BudgetDecision( } /** - * 후보 비용을 포함해 provider admission을 판단한 결과인지 반환합니다. + * Returns whether this result evaluates provider admission with candidate cost included. */ public boolean isAdmissionDecision() { return evaluationType == EvaluationType.ADMISSION; @@ -65,12 +66,12 @@ public boolean isAdmissionDecision() { public enum EvaluationType { /** - * 후보 비용 없는 조회 전용 결과입니다. Provider 호출 허가로 사용할 수 없습니다. + * Query-only result without candidate cost. It cannot authorize provider invocation. */ STATUS, /** - * 후보 비용을 포함한 admission 판단 결과입니다. + * Admission decision that includes candidate cost. */ ADMISSION } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetEvaluator.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetEvaluator.java index 76300b5..72dda36 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetEvaluator.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetEvaluator.java @@ -6,29 +6,30 @@ /** - * 부수 효과 없이 예산 상태를 판단하는 인터페이스입니다. + * Interface for determining budget state without side effects. *

- * 구현체는 판단 결과를 구조화된 {@link BudgetDecision}으로 반환하며 provider 호출을 직접 - * 차단하거나 알림/metric listener를 호출하지 않습니다. Provider 경계는 반환된 decision을 - * 별도로 집행해야 합니다. + * Implementations return a structured {@link BudgetDecision}; they do not block + * provider invocation or call notification/metric listeners directly. The + * provider boundary must enforce the returned decision separately. */ public interface BudgetEvaluator { /** - * 현재 확정 사용량만 조회합니다. + * Reads only the currently committed usage. * - * @return {@link BudgetDecision.EvaluationType#STATUS}인 조회 전용 결과. 후보 비용이 없으므로 - * provider 호출 허가의 근거로 사용할 수 없습니다. + * @return query-only {@link BudgetDecision.EvaluationType#STATUS} result. It + * cannot authorize provider invocation because it has no candidate cost. */ BudgetDecision evaluate(Map tags); /** - * 후보 요청의 통화가 포함된 안전 상한 비용을 더해 admission 상태를 판단합니다. + * Determines admission state after adding the candidate request's + * currency-aware safe upper-bound cost. *

- * {@code projectedUsage >= limit}이면 BLOCK입니다. BLOCK과 CURRENCY_MISMATCH도 예외를 - * 던지지 않고 decision으로 반환합니다. + * {@code projectedUsage >= limit} produces BLOCK. BLOCK and CURRENCY_MISMATCH + * are returned as decisions rather than thrown as exceptions. * - * @return {@link BudgetDecision.EvaluationType#ADMISSION}인 판단 결과 + * @return {@link BudgetDecision.EvaluationType#ADMISSION} decision result */ BudgetDecision evaluate( Map tags, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetKey.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetKey.java index 4c1ecd5..e2aa522 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetKey.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetKey.java @@ -3,12 +3,12 @@ import java.util.Objects; /** - * 월별 예산 bucket을 식별하는 불변 key입니다. + * Immutable key identifying a monthly budget bucket. * - * @param policyId 예산 정책 식별자 - * @param targetType 예산 대상 종류 - * @param targetId 예산 대상 식별자 - * @param window 월별 예산 기간 + * @param policyId budget policy identifier + * @param targetType budget target type + * @param targetId budget target identifier + * @param window monthly budget period */ public record BudgetKey( String policyId, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetPolicy.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetPolicy.java index cb72e76..cf855a6 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetPolicy.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetPolicy.java @@ -6,15 +6,15 @@ import java.util.Objects; /** - * 월별 예산 key 생성과 금액 검증에 사용하는 정책 snapshot입니다. - * 대상 tag가 없고 {@code fallbackTargetId}도 설정되지 않으면 평가는 fail-closed 됩니다. + * Policy snapshot used to create monthly budget keys and validate amounts. + * Evaluation fails closed when the target tag and {@code fallbackTargetId} are both absent. * - * @param id 정책 식별자 - * @param targetType 예산 대상 종류 - * @param targetTagKey 대상 식별자를 읽을 tag key - * @param fallbackTargetId 누락된 대상에 사용할 명시적 fallback, 미설정 시 {@code null} - * @param monthlyLimit 통화를 포함한 월별 한도 - * @param zoneId 월 경계를 계산할 시간대 + * @param id policy identifier + * @param targetType budget target type + * @param targetTagKey tag key from which to read the target identifier + * @param fallbackTargetId explicit fallback for a missing target, or {@code null} when unset + * @param monthlyLimit monthly limit including its currency + * @param zoneId time zone used to calculate month boundaries */ public record BudgetPolicy( String id, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservation.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservation.java index 1657acc..3d552b9 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservation.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservation.java @@ -8,7 +8,7 @@ import java.util.Optional; /** - * 예산 bucket에 생성된 immutable 예약 snapshot입니다. + * Immutable reservation snapshot created in a budget bucket. */ public record BudgetReservation( ReservationId id, @@ -74,8 +74,8 @@ public record BudgetReservation( } /** - * @deprecated request ID와 idempotency key를 같은 값으로 사용하는 호환 생성자입니다. - * 신규 예약은 {@link #reserved(ReservationId, BudgetReservationRequest, Instant)}로 생성하세요. + * @deprecated Compatibility constructor that uses the same value for request ID and idempotency key. + * Create new reservations with {@link #reserved(ReservationId, BudgetReservationRequest, Instant)}. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetReservation( @@ -131,11 +131,13 @@ public static BudgetReservation reserved( } /** - * 같은 idempotency key에 전달된 요청이 기존 예약과 같은 비용 책임을 뜻하는지 비교합니다. + * Compares whether a request using the same idempotency key represents the + * same billing liability as the existing reservation. * - *

{@link PricingSnapshot#checkedAt()}은 가격 조회 시점의 관측 metadata이므로 비교에서 - * 제외합니다. 반면 model, pricing policy, catalog version, currency, rate와 token estimate를 - * 포함해 실제 가격 또는 예약 책임을 바꾸는 값은 모두 일치해야 합니다.

+ *

{@link PricingSnapshot#checkedAt()} is observational metadata from the + * pricing lookup time and is excluded from comparison. Values that could + * change the actual price or reservation liability—including model, pricing + * policy, catalog version, currency, rate, and token estimate—must all match.

*/ public boolean matches(BudgetReservationRequest request) { return key.equals(request.key()) @@ -169,7 +171,7 @@ private static boolean hasSamePricingTerms( ); } - /** 이 예약이 지정한 provider 요청에 속하는지 확인합니다. */ + /** Returns whether this reservation belongs to the specified provider request. */ public boolean belongsTo(String candidateRequestId) { return requestId.equals(candidateRequestId); } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationRequest.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationRequest.java index bd7a433..3cd6aa5 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationRequest.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationRequest.java @@ -7,11 +7,12 @@ import java.util.Optional; /** - * 호출 전 안전 상한 비용을 예산 bucket에 예약하기 위한 immutable 요청입니다. + * Immutable request for reserving a pre-call safe upper-bound cost in a budget bucket. * - *

{@code limit}은 bucket 생성 시 고정되는 정책 snapshot이고, - * {@code safeUpperBoundCost}는 예약할 실제 금액입니다. 모델과 가격 식별자는 - * 같은 idempotency key의 요청 payload가 바뀌었는지 검증하는 fingerprint로 사용됩니다.

+ *

{@code limit} is the policy snapshot fixed when the bucket is created, and + * {@code safeUpperBoundCost} is the amount to reserve. Model and pricing + * identifiers form part of the fingerprint used to detect a changed request + * payload under the same idempotency key.

*/ public record BudgetReservationRequest( BudgetKey key, @@ -68,8 +69,8 @@ public record BudgetReservationRequest( } /** - * @deprecated request ID와 idempotency key를 같은 값으로 사용하는 호환 생성자입니다. - * 신규 호출은 두 값을 명시하는 canonical 생성자를 사용하세요. + * @deprecated Compatibility constructor that uses the same value for request ID and idempotency key. + * New calls should use the canonical constructor with both values explicit. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetReservationRequest( @@ -96,8 +97,8 @@ public BudgetReservationRequest( } /** - * @deprecated request ID와 idempotency key를 같은 값으로 사용하는 호환 생성자입니다. - * 신규 호출은 두 값을 명시하는 canonical 생성자를 사용하세요. + * @deprecated Compatibility constructor that uses the same value for request ID and idempotency key. + * New calls should use the canonical constructor with both values explicit. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetReservationRequest( @@ -125,7 +126,7 @@ public BudgetReservationRequest( } /** - * @deprecated request ID와 idempotency key를 같은 값으로 사용하는 호환 생성자입니다. + * @deprecated Compatibility constructor that uses the same value for request ID and idempotency key. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetReservationRequest( @@ -149,7 +150,7 @@ public BudgetReservationRequest( } /** - * @deprecated request ID와 idempotency key를 같은 값으로 사용하는 호환 생성자입니다. + * @deprecated Compatibility constructor that uses the same value for request ID and idempotency key. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetReservationRequest( diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationResult.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationResult.java index 2d4f485..15ddc60 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationResult.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetReservationResult.java @@ -3,7 +3,7 @@ import java.util.Objects; /** - * 원자적 예산 예약 시도의 결과입니다. + * Result of an atomic budget reservation attempt. */ public record BudgetReservationResult( ReservationStatus status, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetSnapshot.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetSnapshot.java index 9897c01..efbb157 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetSnapshot.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetSnapshot.java @@ -7,7 +7,7 @@ import java.util.Set; /** - * 예산 bucket의 framework-independent 읽기 snapshot입니다. + * Framework-independent read snapshot of a budget bucket. */ public record BudgetSnapshot( BudgetKey key, @@ -45,7 +45,7 @@ public static BudgetSnapshot empty(BudgetKey key, Cost limit) { } /** - * 예약과 미해결 정산 부채를 포함한 admission 기준 사용량입니다. + * Usage used for admission, including reservations and unresolved reconciliation liability. */ public Cost effectiveUsage() { return committedCost @@ -54,7 +54,7 @@ public Cost effectiveUsage() { } /** - * 사용량을 반영한 남은 예산입니다. 초과 상태에서는 0입니다. + * Remaining budget after usage. Returns zero when the budget is exceeded. */ public Cost remaining() { Cost effectiveUsage = effectiveUsage(); diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetState.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetState.java index 21368c9..784e97c 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetState.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetState.java @@ -1,12 +1,12 @@ package io.tokenpilot.budget; /** - * 예산 평가 결과 상태를 나타냅니다. + * Represents the result state of a budget evaluation. *

- * ALLOW : 호출 허용 - * WARN : 예산 경고 - * BLOCK : 호출 차단 - * CURRENCY_MISMATCH : 예산과 비용 통화 불일치 + * ALLOW : call allowed + * WARN : budget warning + * BLOCK : call blocked + * CURRENCY_MISMATCH : budget and cost currencies differ */ public enum BudgetState { ALLOW, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java index cbc545e..4ffb101 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetStateStore.java @@ -3,35 +3,39 @@ import io.tokenpilot.core.domain.Cost; /** - * resolved {@link BudgetKey}별 누적 비용과 limit/currency snapshot을 관리합니다. + * Manages accumulated cost and limit/currency snapshots for each resolved + * {@link BudgetKey}. */ public interface BudgetStateStore { /** - * 기존 확정 비용 조회 API입니다. 예약 금액은 포함하지 않습니다. + * Legacy API for reading committed cost. Reserved amounts are not included. * - *

새 provider admission 경계는 {@link #snapshot(BudgetKey, Cost)} 또는 - * {@link #checkAndReserve(BudgetReservationRequest)}를 사용해야 합니다.

+ *

New provider admission boundaries must use + * {@link #snapshot(BudgetKey, Cost)} or + * {@link #checkAndReserve(BudgetReservationRequest)}.

*/ Cost getAccumulatedCost(BudgetKey key, Cost limit); /** - * 기존 확정 비용 누적 API입니다. 예약 lifecycle을 변경하지 않습니다. + * Legacy API for adding committed cost. It does not change the reservation lifecycle. */ void addCost(BudgetKey key, Cost limit, Cost amount); /** - * 조회와 안전 상한 예약을 하나의 원자적 연산으로 수행합니다. + * Performs the read and safe upper-bound reservation as one atomic operation. * - *

기존 구현체와의 source 호환성을 위해 기본 구현은 지원하지 않음을 명시적으로 - * 반환합니다. 예약을 지원하는 구현체는 이 메서드 또는 요청 overload를 구현해야 합니다.

+ *

For source compatibility with existing implementations, the default + * implementation explicitly returns that the operation is unsupported. + * Implementations that support reservations must implement this method or the + * request overload.

* - * @param key 예산 bucket 식별자 - * @param limit bucket에 고정할 예산 limit snapshot - * @param safeUpperBoundCost 예약할 보수적 비용 상한 - * @param idempotencyKey 중복 요청 식별자 - * @return 생성·재사용·차단·충돌·통화 불일치 결과 - * @deprecated request ID와 idempotency key를 분리하는 overload 또는 요청 객체를 사용하세요. + * @param key budget bucket identifier + * @param limit budget limit snapshot fixed for the bucket + * @param safeUpperBoundCost conservative cost bound to reserve + * @param idempotencyKey duplicate-request identifier + * @return created, reused, blocked, conflict, or currency-mismatch result + * @deprecated Use an overload or request object that separates request ID and idempotency key. */ @Deprecated(since = "0.1.0", forRemoval = false) default BudgetReservationResult checkAndReserve( @@ -49,9 +53,9 @@ default BudgetReservationResult checkAndReserve( } /** - * typed idempotency key를 사용하는 원자적 예약 overload입니다. + * Atomic reservation overload using a typed idempotency key. * - * @deprecated request ID와 idempotency key를 분리하는 overload 또는 요청 객체를 사용하세요. + * @deprecated Use an overload or request object that separates request ID and idempotency key. */ @Deprecated(since = "0.1.0", forRemoval = false) default BudgetReservationResult checkAndReserve( @@ -72,7 +76,8 @@ default BudgetReservationResult checkAndReserve( } /** - * 요청 상관관계와 중복 방지 식별자를 분리하는 원자적 예약 overload입니다. + * Atomic reservation overload separating request correlation from the + * duplicate-prevention identifier. */ default BudgetReservationResult checkAndReserve( BudgetKey key, @@ -95,7 +100,7 @@ default BudgetReservationResult checkAndReserve( } /** - * 모델·가격 snapshot metadata를 포함한 원자적 예약 요청입니다. + * Atomic reservation request including model and pricing snapshot metadata. */ default BudgetReservationResult checkAndReserve(BudgetReservationRequest request) { throw new UnsupportedOperationException( @@ -104,8 +109,8 @@ default BudgetReservationResult checkAndReserve(BudgetReservationRequest request } /** - * 예약과 미해결 정산 부채를 포함한 읽기 snapshot입니다. - * 기존 저장소는 확정 비용만 포함한 호환 snapshot을 반환할 수 있습니다. + * Read snapshot including reservations and unresolved reconciliation liability. + * Legacy stores may return a compatible snapshot containing committed cost only. */ default BudgetSnapshot snapshot(BudgetKey key, Cost limit) { Cost committedCost = getAccumulatedCost(key, limit); @@ -120,7 +125,7 @@ default BudgetSnapshot snapshot(BudgetKey key, Cost limit) { } /** - * {@link #snapshot(BudgetKey, Cost)}의 명시적인 query alias입니다. + * Explicit query alias for {@link #snapshot(BudgetKey, Cost)}. */ default BudgetSnapshot getSnapshot(BudgetKey key, Cost limit) { return snapshot(key, limit); diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetThreshold.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetThreshold.java index 82976d6..f274ce7 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetThreshold.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetThreshold.java @@ -1,19 +1,19 @@ package io.tokenpilot.budget; /** - * 예산 사용률에 따른 임계치 정의 + * Threshold definitions based on budget utilization. * - * NONE : 임계치 미도달 - * HALF : 50% 도달 - * WARNING : 80% 도달 - * EXCEEDED : 100% 초과 + * NONE : threshold not reached + * HALF : 50% reached + * WARNING : 80% reached + * EXCEEDED : 100% exceeded * - * notification 모듈에서 알림 판단 기준으로 사용됨 + * Used by the notification module as the alerting criterion. */ public enum BudgetThreshold { - NONE, // 아직 알림 필요 없음 + NONE, // No notification is needed yet. HALF, // 50% WARNING, // 80% EXCEEDED // 100% -} \ No newline at end of file +} diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetWindow.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetWindow.java index a5d13b3..0cb6853 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetWindow.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/BudgetWindow.java @@ -6,9 +6,9 @@ import java.util.Objects; /** - * 설정된 시간대 기준의 월별 예산 기간입니다. + * Monthly budget period in the configured time zone. * - * @param value 예산이 적용되는 연월 + * @param value year and month to which the budget applies */ public record BudgetWindow(YearMonth value) { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/IdempotencyKey.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/IdempotencyKey.java index 9640c1b..5154e5b 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/IdempotencyKey.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/IdempotencyKey.java @@ -1,7 +1,7 @@ package io.tokenpilot.budget; /** - * 같은 요청의 중복 예약을 식별하는 불변 키입니다. + * Immutable key identifying duplicate reservations for the same request. */ public record IdempotencyKey(String value) { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccounting.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccounting.java index 9840e52..8076ba1 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccounting.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccounting.java @@ -6,16 +6,16 @@ import java.util.Objects; /** - * 예약의 회계 상태와 금액을 변경하는 단일 진입점입니다. + * Single entry point for changing a reservation's accounting state and amounts. * * - * + * * * - * - * - * - * + * + * + * + * * * * @@ -23,7 +23,7 @@ * * * - * + * * * * @@ -64,22 +64,26 @@ * *
예약 회계 명령의 허용 전이와 금액 이동Allowed reservation accounting transitions and amount movements
명령허용 상태결과 상태금액 이동CommandAllowed stateResulting stateAmount movement
{@link #markInFlight(ReservationId)}{@link ReservationState#RESERVED}{@link ReservationState#IN_FLIGHT}없음None
{@code release(CANCELLED_BEFORE_DISPATCH)}
* - *

표의 전이가 새로 적용되면 {@link AccountingTransitionStatus#APPLIED}입니다. - * 동일한 종료 명령과 값 또는 정산 대기 명령의 재호출은 상태와 금액을 유지하고 - * {@link AccountingTransitionStatus#REUSED}, 다른 actual 또는 상충하는 종료 명령은 - * {@link AccountingTransitionStatus#CONFLICT}입니다. 아직 종료 명령이 적용되지 않았지만 - * 현재 상태가 표의 허용 상태가 아니면 {@link AccountingTransitionStatus#NOT_ALLOWED}입니다.

+ *

A transition newly applied from the table returns + * {@link AccountingTransitionStatus#APPLIED}. Repeating the same terminal + * command and values, or a reconciliation-pending command, preserves state and + * amounts and returns {@link AccountingTransitionStatus#REUSED}. Different + * actual usage or a conflicting terminal command returns + * {@link AccountingTransitionStatus#CONFLICT}. If no terminal command has been + * applied but the current state is not allowed by the table, + * {@link AccountingTransitionStatus#NOT_ALLOWED} is returned.

* - *

존재하지 않는 예약과 명령에 허용되지 않은 reason은 상태를 변경하기 전에 - * {@link IllegalArgumentException}으로 거부합니다. 모든 상태와 금액 변경은 같은 budget - * bucket의 임계 구역 안에서 함께 적용됩니다.

+ *

Missing reservations and reasons not allowed for a command are rejected + * with {@link IllegalArgumentException} before state changes. Every state and + * amount change is applied together inside the critical section for the same + * budget bucket.

*/ public interface ReservationAccounting { - /** 예약을 사용한 provider 호출 시작을 기록합니다. */ + /** Records the start of provider invocation using the reservation. */ ReservationTransition markInFlight(ReservationId reservationId); - /** provider 호출 전에 사용하지 않은 예약을 해제합니다. */ + /** Releases an unused reservation before provider invocation. */ default ReservationTransition releaseBeforeDispatch(ReservationId reservationId) { return release( reservationId, @@ -87,7 +91,7 @@ default ReservationTransition releaseBeforeDispatch(ReservationId reservationId) ); } - /** provider가 미과금을 확인한 진행 중 예약을 해제합니다. */ + /** Releases an in-flight reservation after the provider confirms no charge. */ default ReservationTransition releaseConfirmedUnbilled(ReservationId reservationId) { return release( reservationId, @@ -101,22 +105,23 @@ ReservationTransition release( ); /** - * pricing snapshot과 token estimate가 없던 호환 예약을 caller가 계산한 actual 비용으로 확정합니다. + * Commits a compatible reservation without a pricing snapshot or token + * estimate using actual cost calculated by the caller. * - *

이 호환 경로는 token/model correlation을 포함한 - * {@link #commit(ActualUsageCommand)} 결과와 회계 이벤트를 만들 수 없으므로, - * 신규 예약에는 usage 기반 API를 사용해야 합니다.

+ *

This compatibility path cannot create the token/model correlation and + * accounting event produced by {@link #commit(ActualUsageCommand)}, so new + * reservations must use the usage-based API.

*/ ReservationTransition commitCost(ReservationId reservationId, Cost actualCost); /** - * provider actual usage를 예약 시점 가격으로 계산하여 확정합니다. - * 응답 모델이 예약 pricing snapshot과 다르면 다른 모델 가격을 request 가격으로 - * 확정하지 않도록 거부합니다. + * Calculates provider actual usage at the reservation-time price and commits it. + * Rejects a different response model so a different model's price is not + * committed under the request price. */ ReservationReconciliation commit(ActualUsageCommand command); - /** actual을 확보하지 못한 예약을 정산 대기로 전환합니다. */ + /** Moves a reservation without actual usage into reconciliation pending. */ default ReservationTransition markReconciliationRequired( ReservationId reservationId ) { @@ -132,8 +137,9 @@ ReservationTransition markReconciliationRequired( ); /** - * response model과 actual usage를 보존하면서 pricing reconciliation 대기로 이동합니다. - * 기존 구현은 command metadata를 보존하지 않는 호환 동작으로 위임할 수 있습니다. + * Moves a reservation into pricing reconciliation pending while preserving + * the response model and actual usage. Existing implementations may delegate + * to compatibility behavior that does not retain command metadata. */ default ReservationTransition markReconciliationRequired( ActualUsageCommand command, @@ -144,14 +150,17 @@ default ReservationTransition markReconciliationRequired( } /** - * 늦게 도착한 provider actual usage를 예약 시점 가격으로 계산하여 확정합니다. - * 응답 모델은 예약 pricing snapshot의 모델과 같아야 합니다. + * Calculates and commits late-arriving provider actual usage at the + * reservation-time price. The response model must match the model in the + * reservation pricing snapshot. */ ReservationReconciliation reconcileLateActual(ActualUsageCommand command); /** - * pending actual에 response model의 명시적 immutable pricing snapshot을 적용합니다. - * 기본 구현은 기존 예약 snapshot만 지원하는 구현과의 호환을 위해 fail-closed합니다. + * Applies an explicit immutable pricing snapshot for the response model to + * pending actual usage. The default implementation fails closed for + * compatibility with implementations that support only the existing + * reservation snapshot. */ default ReservationReconciliation reconcileLateActual( ActualUsageCommand command, @@ -168,16 +177,17 @@ default ReservationReconciliation reconcileLateActual( } /** - * pricing snapshot과 token estimate가 없던 pending 호환 예약을 caller가 계산한 actual 비용으로 확정합니다. + * Commits a pending compatible reservation without a pricing snapshot or + * token estimate using actual cost calculated by the caller. * - *

신규 예약에는 {@link #reconcileLateActual(ActualUsageCommand)}를 사용해야 합니다.

+ *

New reservations must use {@link #reconcileLateActual(ActualUsageCommand)}.

*/ ReservationTransition reconcileLateActualCost( ReservationId reservationId, Cost actualCost ); - /** 후속 정산할 수 없는 pending 예약을 명시적으로 상각합니다. */ + /** Explicitly writes off a pending reservation that cannot be reconciled later. */ default ReservationTransition writeOff(ReservationId reservationId) { return writeOff( reservationId, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingEvent.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingEvent.java index 00438d9..b4d04aa 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingEvent.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingEvent.java @@ -2,7 +2,7 @@ import java.util.Objects; -/** 새롭게 적용된 예약 정산을 전달하는 회계 이벤트입니다. */ +/** Accounting event for a newly applied reservation settlement. */ public record ReservationAccountingEvent( ReservationReconciliation reconciliation ) { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListener.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListener.java index e0d509e..c95fb2d 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListener.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListener.java @@ -1,12 +1,14 @@ package io.tokenpilot.budget; /** - * 예약 admission과 정산 결과를 수신하는 framework-independent 계약입니다. + * Framework-independent contract for receiving reservation admission and + * settlement results. * - *

기존 lambda listener와의 source 호환성을 위해 {@link #onCommitted}만 추상 - * 메서드로 유지합니다. 나머지 callback은 예약 저장소가 확정한 결과를 관찰하기 위한 - * 선택적 확장점입니다. 회계 변경 callback은 새로 적용된 전이에만 전달되고, 예약 - * callback은 모든 평가 결과와 BLOCK 결과를 각각 관찰할 수 있습니다.

+ *

Only {@link #onCommitted} remains abstract for source compatibility with + * existing lambda listeners. The other callbacks are optional extension points + * for observing results confirmed by the reservation store. Accounting-change + * callbacks are delivered only for newly applied transitions; reservation + * callbacks can observe every evaluation and BLOCK result separately.

*/ @FunctionalInterface public interface ReservationAccountingListener { @@ -14,8 +16,9 @@ public interface ReservationAccountingListener { void onCommitted(ReservationAccountingEvent event); /** - * 적용된 accounting transition과 같은 linearization point의 bucket snapshot을 전달합니다. - * 기존 listener는 {@link #onCommitted(ReservationAccountingEvent)}로 위임됩니다. + * Delivers the bucket snapshot at the same linearization point as the applied + * accounting transition. Existing listeners delegate to + * {@link #onCommitted(ReservationAccountingEvent)}. */ default void onAccountingApplied( ReservationAccountingEvent event, @@ -24,26 +27,26 @@ default void onAccountingApplied( onCommitted(event); } - /** actual 미확정 estimate가 pending liability로 이동한 결과를 전달합니다. */ + /** Delivers a result where an estimate with unconfirmed actual usage moved to pending liability. */ default void onReconciliationRequired( ReservationReconciliationRequiredEvent event ) { } - /** 원자적 예약 시도의 모든 결과를 전달합니다. */ + /** Delivers every result of an atomic reservation attempt. */ default void onReservationEvaluated( BudgetReservationRequest request, BudgetReservationResult result ) { } - /** 원자적 admission에서 상태 변경 없이 차단된 결과를 전달합니다. */ + /** Delivers a result blocked during atomic admission without changing state. */ default void onReservationBlocked( BudgetReservationRequest request, BudgetReservationResult result ) { } - /** metric tag에 안전한 제한된 listener 종류를 반환합니다. */ + /** Returns a bounded listener type safe to use as a metric tag. */ default ReservationAccountingListenerType listenerType() { return ReservationAccountingListenerType.CUSTOM; } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerErrorHandler.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerErrorHandler.java index a97c4a4..ce6ff2c 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerErrorHandler.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerErrorHandler.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget; -/** bounded 회계 listener 실패를 관찰하는 best-effort hook입니다. */ +/** Best-effort hook for observing bounded accounting listener failures. */ @FunctionalInterface public interface ReservationAccountingListenerErrorHandler { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerFailureEvent.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerFailureEvent.java index 89684b0..918b885 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerFailureEvent.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerFailureEvent.java @@ -3,10 +3,10 @@ import java.util.Objects; /** - * 회계 listener callback 실패를 bounded 정보만으로 전달하는 이벤트입니다. + * Event carrying an accounting listener callback failure using only bounded information. * - *

원본 예외와 메시지를 포함하지 않아 prompt, provider 응답 또는 자격 증명이 - * 관측 경로로 전달되지 않습니다.

+ *

The original exception and message are excluded so prompts, provider + * responses, and credentials do not reach the observation path.

*/ public record ReservationAccountingListenerFailureEvent( ReservationAccountingListenerType listenerType, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerPhase.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerPhase.java index 93642ce..08d0471 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerPhase.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerPhase.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget; -/** 회계 listener 실패 관측에 사용하는 제한된 callback 단계입니다. */ +/** Bounded callback phases used to observe accounting listener failures. */ public enum ReservationAccountingListenerPhase { ACCOUNTING_APPLIED, RECONCILIATION_REQUIRED, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerType.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerType.java index 8fac5d8..7542254 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerType.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingListenerType.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget; -/** 회계 listener 실패 관측에 사용하는 제한된 listener 종류입니다. */ +/** Bounded listener types used to observe accounting listener failures. */ public enum ReservationAccountingListenerType { METRICS, NOTIFICATION, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingReason.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingReason.java index 6e85725..f5b7400 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingReason.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationAccountingReason.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget; -/** 회계 명령과 정산 결과에 사용하는 제한된 사유입니다. */ +/** Bounded reasons used by accounting commands and settlement results. */ public enum ReservationAccountingReason { ACTUAL_USAGE_REPORTED, LATE_ACTUAL_USAGE_REPORTED, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationActualTokens.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationActualTokens.java index d5e377b..c618bf4 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationActualTokens.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationActualTokens.java @@ -7,7 +7,7 @@ import java.util.Map; import java.util.Objects; -/** Provider actual usage에서 무제한 metadata를 제외한 회계용 token 결과입니다. */ +/** Accounting token result derived from provider actual usage without unbounded metadata. */ public record ReservationActualTokens( long inputTokens, long outputTokens, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationId.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationId.java index e22dcf6..276f8cf 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationId.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationId.java @@ -3,7 +3,7 @@ import java.util.UUID; /** - * 예산 예약을 식별하는 불변 ID입니다. + * Immutable ID identifying a budget reservation. */ public record ReservationId(String value) { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliation.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliation.java index e8cb910..1885541 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliation.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliation.java @@ -8,7 +8,7 @@ import java.util.Objects; /** - * 한 예약의 estimate와 actual 비용을 연결한 회계 정산 결과입니다. + * Accounting settlement result linking one reservation's estimate and actual cost. */ public record ReservationReconciliation( String requestId, @@ -16,9 +16,9 @@ public record ReservationReconciliation( ReservationId reservationId, BudgetKey budgetKey, String responseModelId, - /** 예약 시점에 estimate를 계산한 request pricing snapshot입니다. */ + /** Request pricing snapshot used to calculate the estimate at reservation time. */ PricingSnapshot pricingSnapshot, - /** provider response model의 actual 비용 계산에 사용한 pricing snapshot입니다. */ + /** Pricing snapshot used to calculate actual cost for the provider response model. */ PricingSnapshot actualPricingSnapshot, ReservationTokenEstimate tokenEstimate, ReservationActualTokens actualTokens, @@ -65,8 +65,9 @@ public record ReservationReconciliation( } /** - * 기존 request pricing snapshot만 사용한 정산 결과를 만드는 호환 생성자입니다. - * request와 response model이 같은 기존 경로에서는 두 snapshot이 동일합니다. + * Compatibility constructor that creates a settlement result using only the + * existing request pricing snapshot. In the existing path where request and + * response models are the same, both snapshots are identical. */ public ReservationReconciliation( String requestId, @@ -118,32 +119,32 @@ private static PricingSnapshot actualSnapshotForResponseModel( ); } - /** 예약 시점 pricing snapshot의 request model입니다. */ + /** Request model in the reservation-time pricing snapshot. */ public String requestModelId() { return pricingSnapshot.modelId(); } - /** actual provider usage에 적용한 response model pricing policy입니다. */ + /** Pricing policy applied to actual provider usage for the response model. */ public String actualPricingPolicyId() { return actualPricingSnapshot.pricingPolicyId(); } - /** actual provider usage에 적용한 response model catalog version입니다. */ + /** Catalog version applied to actual provider usage for the response model. */ public String actualCatalogVersion() { return actualPricingSnapshot.catalogVersion(); } - /** 예약 시점 pricing policy 식별자입니다. */ + /** Pricing policy identifier at reservation time. */ public String pricingPolicyId() { return pricingSnapshot.pricingPolicyId(); } - /** 예약 시점 model catalog version입니다. */ + /** Model catalog version at reservation time. */ public String catalogVersion() { return pricingSnapshot.catalogVersion(); } - /** actual에서 estimate를 뺀 signed 비용 차이입니다. */ + /** Signed cost difference obtained by subtracting the estimate from actual cost. */ public BigDecimal delta() { return actual.value().subtract(estimate.value()); } @@ -170,7 +171,7 @@ public long totalTokenDelta() { return Math.subtractExact(actualTokens.totalTokens(), estimatedTotal); } - /** estimate와 actual이 사용하는 통화입니다. */ + /** Currency used by the estimate and actual cost. */ public Currency currency() { return actual.currency(); } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliationRequiredEvent.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliationRequiredEvent.java index 120e4ee..bdfd81d 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliationRequiredEvent.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationReconciliationRequiredEvent.java @@ -3,7 +3,7 @@ import java.util.Objects; import java.util.Optional; -/** actual usage를 확보하지 못해 pending liability로 이동한 원자적 회계 결과입니다. */ +/** Atomic accounting result moved to pending liability because actual usage was unavailable. */ public record ReservationReconciliationRequiredEvent( ReservationId reservationId, BudgetKey budgetKey, @@ -57,7 +57,7 @@ public record ReservationReconciliationRequiredEvent( } } - /** 기존 actual-unavailable 이벤트를 만드는 호환 생성자입니다. */ + /** Compatibility constructor for the former actual-unavailable event. */ public ReservationReconciliationRequiredEvent( ReservationId reservationId, BudgetKey budgetKey, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationState.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationState.java index 4a1cd07..5c376e5 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationState.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationState.java @@ -1,31 +1,31 @@ package io.tokenpilot.budget; /** - * 예산 예약의 현재 회계 상태입니다. + * Current accounting state of a budget reservation. */ public enum ReservationState { - /** 안전 상한액이 예약되었지만 공급자 호출은 시작되지 않은 상태입니다. */ + /** The safe upper-bound amount is reserved, but provider invocation has not started. */ RESERVED, - /** 예약을 사용해 공급자 호출이 진행 중인 상태입니다. */ + /** Provider invocation is in progress using the reservation. */ IN_FLIGHT, - /** 실제 사용량을 알 수 없어 후속 정산이 필요한 상태입니다. */ + /** Actual usage is unavailable and later reconciliation is required. */ RECONCILIATION_REQUIRED, - /** 실제 사용 금액이 확정된 종료 상태입니다. */ + /** Terminal state with actual cost committed. */ COMMITTED, - /** 사용되지 않은 예약 금액이 해제된 종료 상태입니다. */ + /** Terminal state with unused reserved cost released. */ RELEASED, - /** 실제 사용량을 끝내 확정할 수 없어 정책에 따라 상각된 종료 상태입니다. */ + /** Terminal state written off by policy because actual usage could not be confirmed. */ WRITTEN_OFF; /** - * 더 이상 정상적인 회계 전이를 허용하지 않는 종료 상태인지 반환합니다. + * Returns whether this is a terminal state that allows no further normal accounting transitions. * - *

{@link #RECONCILIATION_REQUIRED}는 후속 정산을 기다리는 상태이므로 종료 상태가 아닙니다.

+ *

{@link #RECONCILIATION_REQUIRED} is not terminal because it is waiting for later reconciliation.

*/ public boolean isClosed() { return this == COMMITTED || this == RELEASED || this == WRITTEN_OFF; diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStateMachine.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStateMachine.java index 812b575..3f65d74 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStateMachine.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStateMachine.java @@ -3,14 +3,14 @@ import java.util.Objects; /** - * 예약의 회계 상태 전이 가능 여부를 판단합니다. + * Determines whether an accounting state transition is allowed for a reservation. */ public final class ReservationStateMachine { private ReservationStateMachine() { } - /** 예약된 요청의 공급자 호출을 시작합니다. */ + /** Starts provider invocation for a reserved request. */ public static ReservationTransition onDispatch(ReservationState currentState) { return transitionFrom( currentState, @@ -19,7 +19,7 @@ public static ReservationTransition onDispatch(ReservationState currentState) { ); } - /** 공급자 호출 전에 예약을 해제합니다. */ + /** Releases a reservation before provider invocation. */ public static ReservationTransition release(ReservationState currentState) { return transitionFrom( currentState, @@ -28,7 +28,7 @@ public static ReservationTransition release(ReservationState currentState) { ); } - /** 공급자가 미과금을 확인한 진행 중 예약을 해제합니다. */ + /** Releases an in-flight reservation after the provider confirms no charge. */ public static ReservationTransition releaseConfirmedUnbilled( ReservationState currentState ) { @@ -39,7 +39,7 @@ public static ReservationTransition releaseConfirmedUnbilled( ); } - /** 전달받은 actual을 확정하기 위한 상태 전이를 판단합니다. */ + /** Determines the state transition needed to commit the received actual usage. */ public static ReservationTransition commit(ReservationState currentState) { return transitionFrom( currentState, @@ -48,7 +48,7 @@ public static ReservationTransition commit(ReservationState currentState) { ); } - /** actual을 전달받지 못한 예약을 정산 대기로 전환할 수 있는지 판단합니다. */ + /** Determines whether a reservation without actual usage can enter reconciliation pending. */ public static ReservationTransition markReconciliationRequired(ReservationState currentState) { return transitionFrom( currentState, @@ -57,7 +57,7 @@ public static ReservationTransition markReconciliationRequired(ReservationState ); } - /** 정산 대기 중 전달받은 late actual을 확정할 수 있는지 판단합니다. */ + /** Determines whether late actual usage can be committed while reconciliation is pending. */ public static ReservationTransition reconcileLateActual(ReservationState currentState) { return transitionFrom( currentState, @@ -66,7 +66,7 @@ public static ReservationTransition reconcileLateActual(ReservationState current ); } - /** 정산 대기 중인 예약을 명시적으로 상각합니다. */ + /** Explicitly writes off a reservation pending reconciliation. */ public static ReservationTransition writeOff(ReservationState currentState) { return transitionFrom( currentState, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStatus.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStatus.java index 08c54de..546021b 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStatus.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationStatus.java @@ -1,7 +1,7 @@ package io.tokenpilot.budget; /** - * 예산 예약 시도 결과입니다. + * Result of a budget reservation attempt. */ public enum ReservationStatus { CREATED, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTokenEstimate.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTokenEstimate.java index 3e59e05..ce5c4f7 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTokenEstimate.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTokenEstimate.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget; -/** 예약 비용의 근거가 된 호출 전 token estimate입니다. */ +/** Pre-call token estimate supporting the reserved cost. */ public record ReservationTokenEstimate( long inputEstimatedTokens, long inputSafeUpperBoundTokens, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTransition.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTransition.java index ed66bc0..1fc58d9 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTransition.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/ReservationTransition.java @@ -3,7 +3,7 @@ import java.util.Objects; /** - * 예약의 상태 전이 판단입니다. + * Reservation state transition decision. */ public record ReservationTransition( ReservationState previousState, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/exception/BudgetExceededException.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/exception/BudgetExceededException.java index 4ca05f2..80cb8a4 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/exception/BudgetExceededException.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/exception/BudgetExceededException.java @@ -5,10 +5,11 @@ import java.util.Objects; /** - * Provider 경계에서 BLOCK decision을 집행할 때 던지는 예외입니다. + * Exception thrown when a BLOCK decision is enforced at the provider boundary. *

- * {@code BudgetEvaluator}는 이 예외를 직접 던지지 않습니다. Provider invocation 직전의 - * 경계가 원래 decision을 보존한 이 예외를 던져 호출 체인을 중단합니다. + * {@code BudgetEvaluator} does not throw this exception directly. The boundary + * immediately before provider invocation throws it while preserving the original + * decision to stop the call chain. */ public class BudgetExceededException extends RuntimeException { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ActualUsageFingerprint.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ActualUsageFingerprint.java index ac017cf..7191a37 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ActualUsageFingerprint.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ActualUsageFingerprint.java @@ -7,7 +7,7 @@ import java.util.Objects; -/** 중복 actual callback을 민감하거나 무제한인 metadata 없이 식별합니다. */ +/** Identifies duplicate actual callbacks without sensitive or unbounded metadata. */ record ActualUsageFingerprint( String requestId, String attemptId, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedCommit.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedCommit.java index 5e4c937..935d647 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedCommit.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedCommit.java @@ -12,7 +12,8 @@ import static io.tokenpilot.budget.AccountingTransitionStatus.REUSED; /** - * 이미 적용된 commit의 종류, actual과 재호출 판단을 보관합니다. + * Stores the type of an applied commit and the values used to evaluate actual + * usage and repeated calls. */ record AppliedCommit( CommitType type, diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedRelease.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedRelease.java index fdc6c8d..092f48e 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedRelease.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/AppliedRelease.java @@ -10,7 +10,7 @@ import static io.tokenpilot.budget.AccountingTransitionStatus.REUSED; /** - * 이미 적용된 release의 종류와 재호출 판단을 보관합니다. + * Stores the type of an applied release and the values used to evaluate repeated calls. */ record AppliedRelease(ReleaseType type) { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/CommitType.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/CommitType.java index e1f780a..25205ff 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/CommitType.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/CommitType.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget.internal; -/** Actual 비용이 확정된 경로입니다. */ +/** Path on which actual cost was committed. */ enum CommitType { DIRECT, LATE_ACTUAL diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java index 19c6497..280c149 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/DefaultBudgetEvaluator.java @@ -17,10 +17,10 @@ import java.util.Objects; /** - * 이 evaluator는 부수 효과 없는 policy projection만 담당합니다. - * {@code projectedUsage = committedUsage + candidateCost}이며 active reservation과 - * reconciliation liability는 {@link BudgetStateStore#snapshot}과 원자적 reservation - * store가 별도로 반영합니다. + * This evaluator is responsible only for side-effect-free policy projection. + * {@code projectedUsage = committedUsage + candidateCost}; active reservations + * and reconciliation liability are reflected separately by + * {@link BudgetStateStore#snapshot} and the atomic reservation store. */ public class DefaultBudgetEvaluator implements BudgetEvaluator { diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStore.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStore.java index 4a7fdb4..c65f39b 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStore.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/InMemoryBudgetStateStore.java @@ -44,10 +44,12 @@ import java.util.function.Supplier; /** - * resolved {@link BudgetKey}별 확정 비용과 원자적 예약을 관리하는 인메모리 저장소입니다. + * In-memory store managing committed cost and atomic reservations for each + * resolved {@link BudgetKey}. * - *

bucket별 monitor가 조회·통화 검증·한도 검증·예약 갱신을 함께 보호하고, - * 별도의 idempotency index가 같은 요청의 중복 예약을 차단합니다.

+ *

A per-bucket monitor protects reads, currency validation, limit validation, + * and reservation updates together. A separate idempotency index prevents + * duplicate reservations for the same request.

*/ public class InMemoryBudgetStateStore implements AtomicBudgetStateStore { @@ -734,7 +736,7 @@ private void publishListenerFailure( try { errorHandler.onFailure(event); } catch (RuntimeException ignored) { - // Error handler도 best-effort이며 회계/admission 결과에 영향을 주지 않습니다. + // The error handler is also best-effort and does not affect accounting or admission results. } } } diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/LedgerBudgetComponents.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/LedgerBudgetComponents.java index 71db80e..fb51a5d 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/LedgerBudgetComponents.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/LedgerBudgetComponents.java @@ -17,7 +17,7 @@ import java.util.function.Supplier; /** - * 예산 제어 컴포넌트 생성을 위한 팩토리 클래스입니다. + * Factory for creating budget-control components. */ public final class LedgerBudgetComponents { @@ -28,12 +28,12 @@ public static BudgetStateStore inMemoryBudgetStateStore() { return new InMemoryBudgetStateStore(); } - /** 예약 admission과 accounting을 한 객체로 노출하는 기본 합성 store입니다. */ + /** Default composite store exposing reservation admission and accounting as one object. */ public static AtomicBudgetStateStore inMemoryAtomicBudgetStateStore() { return new InMemoryBudgetStateStore(); } - /** 예약과 회계를 같은 원자적 상태에 연결하는 합성 store factory입니다. */ + /** Composite store factory connecting reservation and accounting to the same atomic state. */ public static AtomicBudgetStateStore inMemoryAtomicBudgetStateStore( Supplier> accountingListenerSupplier, Supplier> @@ -48,7 +48,7 @@ public static AtomicBudgetStateStore inMemoryAtomicBudgetStateStore( ); } - /** PR #68 accounting/notification listener 연결과 호환되는 convenience factory입니다. */ + /** Convenience factory compatible with the PR #68 accounting/notification listener wiring. */ public static BudgetStateStore inMemoryBudgetStateStore( List accountingListeners ) { @@ -60,7 +60,7 @@ public static BudgetStateStore inMemoryBudgetStateStore( ); } - /** listener 실패 관측 hook까지 연결하는 convenience factory입니다. */ + /** Convenience factory that also connects a listener-failure observation hook. */ public static BudgetStateStore inMemoryBudgetStateStore( List accountingListeners, List listenerErrorHandlers @@ -74,7 +74,7 @@ public static BudgetStateStore inMemoryBudgetStateStore( ); } - /** Spring 등 DI container가 listener를 store 생성 뒤 지연 해석할 수 있는 factory입니다. */ + /** Factory allowing a DI container such as Spring to resolve listeners after store creation. */ public static BudgetStateStore inMemoryBudgetStateStore( Supplier> accountingListenerSupplier, Supplier> @@ -139,7 +139,7 @@ public static BudgetStateStore inMemoryBudgetStateStore( } /** - * 예약을 생성한 store와 동일한 객체의 회계 명령 진입점을 반환합니다. + * Returns the accounting command entry point on the same object that created the reservation. */ public static ReservationAccounting reservationAccounting( BudgetStateStore stateStore diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReleaseType.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReleaseType.java index 79f5fe1..2a778b9 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReleaseType.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReleaseType.java @@ -1,6 +1,6 @@ package io.tokenpilot.budget.internal; -/** 예약 금액이 해제된 경로입니다. */ +/** Path on which the reserved amount was released. */ enum ReleaseType { BEFORE_DISPATCH, CONFIRMED_UNBILLED diff --git a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReservationAccountingState.java b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReservationAccountingState.java index bb06898..717c6c6 100644 --- a/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReservationAccountingState.java +++ b/token-pilot-budget/src/main/java/io/tokenpilot/budget/internal/ReservationAccountingState.java @@ -15,7 +15,7 @@ import static io.tokenpilot.budget.ReservationState.WRITTEN_OFF; /** - * 한 예약에 적용된 회계 명령을 판단하기 위한 내부 상태입니다. + * Internal state used to evaluate accounting commands applied to one reservation. */ final class ReservationAccountingState { diff --git a/token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/BudgetReservationConcurrencyTest.java b/token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/BudgetReservationConcurrencyTest.java index 870fce1..1edda1b 100644 --- a/token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/BudgetReservationConcurrencyTest.java +++ b/token-pilot-budget/src/test/java/io/tokenpilot/budget/internal/BudgetReservationConcurrencyTest.java @@ -35,11 +35,13 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * #36 예약 linearization point의 결정적 동시성 회귀 테스트입니다. + * Deterministic concurrency regression test for the #36 reservation linearization point. * - *

같은 시작선에서 경쟁 명령을 실행하고 공개 snapshot만 검증합니다. 이 테스트는 - * 동시성 정확성의 수학적 증명이나 선형성 검증기를 대체하지 않습니다. #37의 - * commit/release/reconciliation 및 #46의 listener 정책은 해당 계약이 구현된 뒤 확장합니다.

+ *

Runs competing commands from the same starting line and verifies only the + * public snapshot. This test is not a mathematical proof of concurrency + * correctness or a replacement for a linearizability checker. The #37 + * commit/release/reconciliation and #46 listener policies are extended after + * their contracts are implemented.

*/ class BudgetReservationConcurrencyTest { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/CoreComponents.java b/token-pilot-core/src/main/java/io/tokenpilot/core/CoreComponents.java index b3750c3..20f3739 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/CoreComponents.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/CoreComponents.java @@ -7,7 +7,7 @@ import java.util.List; /** - * framework-independent core component entrypoint입니다. + * Framework-independent core component entry point. */ public final class CoreComponents { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/CostCalculator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/CostCalculator.java index ca5e8da..de295bb 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/CostCalculator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/CostCalculator.java @@ -8,19 +8,19 @@ import java.util.Objects; /** - * 사용량과 가격 정책을 바탕으로 비용을 계산하는 인터페이스. + * Interface for calculating cost from usage and a pricing policy. */ public interface CostCalculator { /** - * 토큰 사용량과 가격 정책을 기반으로 비용을 산출합니다. - * @param usage 토큰 사용량 - * @param plan 가격 정책 - * @return 산출된 비용 + * Calculates cost from token usage and a pricing policy. + * @param usage token usage + * @param plan pricing policy + * @return calculated cost */ Cost calculate(TokenUsage usage, PricingPlan plan); /** - * 예약 시점에 고정한 가격 snapshot으로 비용을 계산합니다. + * Calculates cost using the pricing snapshot fixed at reservation time. */ default Cost calculate(TokenUsage usage, PricingSnapshot snapshot) { Objects.requireNonNull(snapshot, "snapshot must not be null"); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerListener.java b/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerListener.java index ddb5288..d7a9ed0 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerListener.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerListener.java @@ -3,14 +3,14 @@ import io.tokenpilot.core.domain.CostRecordedEvent; /** - * 비용 기록 이벤트를 수신하는 리스너 인터페이스. - * 옵저버 패턴의 Observer 역할을 수행하며, 순수 자바 기반으로 동작합니다. + * Listener interface for cost-recording events. + * It serves as the Observer in the observer pattern and runs on plain Java. */ @FunctionalInterface public interface LedgerListener { /** - * 비용 기록 이벤트가 발생했을 때 호출됩니다. - * @param event 발생한 비용 기록 이벤트 + * Called when a cost-recording event occurs. + * @param event emitted cost-recording event */ void onRecord(CostRecordedEvent event); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java b/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java index e4a96dd..b13d99e 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/LedgerManager.java @@ -8,33 +8,33 @@ import java.util.Map; /** - * AI 호출에 대한 지출을 기록하고 관리하는 통합 매니저 인터페이스. + * Unified manager interface for recording and managing AI call spending. */ public interface LedgerManager { /** - * 특정 모델의 호출 정보를 기록하고 최종 비용을 계산합니다. - * @param modelId 모델 식별자 - * @param usage 토큰 사용량 - * @param tags 추가 메타데이터 (tenant_id, user_id 등) - * @return 산출된 비용 + * Records a call for a model and calculates the final cost. + * @param modelId model identifier + * @param usage token usage + * @param tags additional metadata, such as tenant_id or user_id + * @return calculated cost */ Cost record(String modelId, TokenUsage usage, Map tags); /** - * 이미 resolve된 가격 정책으로 호출 정보를 기록하고 최종 비용을 계산합니다. - * @param plan provider 호출 전에 resolve된 가격 정책 - * @param usage 토큰 사용량 - * @param tags 추가 메타데이터 (tenant_id, user_id 등) - * @return 산출된 비용 + * Records a call and calculates the final cost using an already resolved pricing policy. + * @param plan pricing policy resolved before provider invocation + * @param usage token usage + * @param tags additional metadata, such as tenant_id or user_id + * @return calculated cost */ Cost record(PricingPlan plan, TokenUsage usage, Map tags); /** - * 요청 단위 pricing snapshot으로 호출 정보를 기록하고 최종 비용을 계산합니다. - * @param snapshot provider 호출 전에 보존된 pricing snapshot - * @param usage 토큰 사용량 - * @param tags 추가 메타데이터 (tenant_id, user_id 등) - * @return 산출된 비용 + * Records a call and calculates the final cost using a request-scoped pricing snapshot. + * @param snapshot pricing snapshot retained before provider invocation + * @param usage token usage + * @param tags additional metadata, such as tenant_id or user_id + * @return calculated cost */ Cost record(PricingSnapshot snapshot, TokenUsage usage, Map tags); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/ModelRegistry.java b/token-pilot-core/src/main/java/io/tokenpilot/core/ModelRegistry.java index 7558726..f2cea59 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/ModelRegistry.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/ModelRegistry.java @@ -5,21 +5,20 @@ import java.util.Optional; /** - * versioned model definition을 canonical id 또는 exact alias로 조회합니다. + * Looks up versioned model definitions by canonical ID or exact alias. * - *

조회 결과의 {@link ModelDefinition#canonicalModelId()}와 - * {@link ModelDefinition#pricingPolicyId()}를 함께 사용해 - * {@link PricingRegistry}에서 가격 정책을 조회해야 합니다. alias 문자열을 - * 가격 조회에 직접 전달하면 context와 pricing이 서로 다른 모델을 가리킬 수 - * 있습니다.

+ *

Use {@link ModelDefinition#canonicalModelId()} and + * {@link ModelDefinition#pricingPolicyId()} from the result together when + * looking up pricing in {@link PricingRegistry}. Passing an alias directly to + * pricing lookup could cause context and pricing to refer to different models.

*/ public interface ModelRegistry { /** - * canonical model id 또는 exact alias를 canonical definition으로 해석합니다. + * Resolves a canonical model ID or exact alias to its canonical definition. * - * @param modelIdOrAlias canonical id 또는 alias - * @return 등록된 모델 definition, 미등록이면 empty + * @param modelIdOrAlias canonical ID or alias + * @return registered model definition, or empty when not registered */ Optional find(String modelIdOrAlias); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightCostEstimator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightCostEstimator.java index cd5d4c0..84ae3aa 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightCostEstimator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightCostEstimator.java @@ -5,32 +5,33 @@ import io.tokenpilot.core.domain.TokenCountResult; /** - * 호출 전에 계산한 REQUEST 범위의 토큰 상한을 금액 상한으로 변환하는 Core 계약입니다. + * Core contract for converting a pre-call REQUEST-scope token bound into a cost bound. * - *

이 계약은 비용을 계산할 뿐 context window 적합성이나 provider 호출 허가를 - * 판정하지 않습니다. 호출자는 요청 토큰 결과가 모델의 context admission을 통과했고, - * {@link PreflightPricingContext}가 계산에 사용할 하나의 불변 pricing snapshot을 - * 보관하는지 먼저 보장해야 합니다. 계산기는 다른 snapshot을 registry에서 다시 - * 조회하지 않습니다.

+ *

This contract calculates cost only; it does not determine context-window + * admission or authorize provider invocation. Callers must first ensure that + * the request token result passes model context admission and that + * {@link PreflightPricingContext} retains one immutable pricing snapshot for + * the calculation. The calculator does not look up another snapshot from a registry.

* - *

계산 가능한 경우에도 예약 근거로 사용할 값은 - * {@link PreflightCostResult.Bounded#safeUpperBoundCost()}뿐입니다. - * {@code estimatedCost}는 관찰과 표시를 위한 값입니다.

+ *

Even when calculation succeeds, only + * {@link PreflightCostResult.Bounded#safeUpperBoundCost()} may support a reservation. + * {@code estimatedCost} is for observation and display.

*/ public interface PreflightCostEstimator { /** - * atomic reservation에 사용할 호출 전 비용 상한을 계산합니다. - * 입력이 REQUEST 범위가 아니거나 tokenizer 기준이 맞지 않거나 가격 snapshot이 - * 없거나 문맥에 snapshot이 없으면 숫자 비용 대신 제한된 unavailable 결과를 반환해야 합니다. + * Calculates the pre-call cost bound used for atomic reservation. + * When the input is not REQUEST scope, the tokenizer basis is incompatible, + * pricing is missing, or the context has no snapshot, returns a bounded + * unavailable result instead of a numeric cost. * - * @param pricingContext canonical model, pricing policy, catalog version과 - * 검증된 pricing 조건을 담은 계산 문맥 - * @param requestInput 실제 전송 요청 전체를 계산한 REQUEST 범위의 token 결과 - * @param reservedOutputTokens 호출 전에 확보할 최대 출력 token 수 - * @return 계산 가능한 비용 상한 또는 그 사유를 담은 unavailable 결과 - * @throws NullPointerException 필수 인자가 {@code null}인 경우 - * @throws IllegalArgumentException reservedOutputTokens가 음수인 경우 + * @param pricingContext calculation context containing the canonical model, + * pricing policy, catalog version, and validated pricing conditions + * @param requestInput REQUEST-scope token result covering the complete request to be sent + * @param reservedOutputTokens maximum output tokens secured before the call + * @return calculable cost bound or an unavailable result containing the reason + * @throws NullPointerException when a required argument is {@code null} + * @throws IllegalArgumentException when reservedOutputTokens is negative */ PreflightCostResult estimate( PreflightPricingContext pricingContext, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightDecisionListener.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightDecisionListener.java index 4621305..e040cfc 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightDecisionListener.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PreflightDecisionListener.java @@ -2,7 +2,7 @@ import io.tokenpilot.core.domain.PreflightDecisionEvent; -/** context admission 판정을 best-effort로 관찰하는 framework-independent 계약입니다. */ +/** Framework-independent contract for best-effort observation of context admission decisions. */ @FunctionalInterface public interface PreflightDecisionListener { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingEvaluator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingEvaluator.java index 16c6976..b6a61a4 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingEvaluator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingEvaluator.java @@ -7,24 +7,24 @@ import java.util.Optional; /** - * Pricing snapshot의 사용 가능 여부와 actual model 정합성을 판단하는 정책 계약. + * Policy contract for determining pricing snapshot availability and actual-model consistency. */ public interface PricingEvaluator { /** - * Snapshot에 요청 처리에 필요한 rate가 있는지 검증합니다. + * Validates that the snapshot contains rates required to process the request. * - * @param snapshot 검증할 pricing snapshot, 조회되지 않은 경우 empty - * @return snapshot 및 필수 rate의 resolution + * @param snapshot pricing snapshot to validate, or empty when it was not resolved + * @return resolution of the snapshot and required rates */ PricingResolution validateSnapshotRates(Optional snapshot); /** - * 호출 전 snapshot을 actual 응답 모델에 적용할 수 있는지 판단합니다. + * Determines whether the pre-call snapshot can be applied to the actual response model. * - * @param snapshot 호출 전에 확정한 pricing snapshot, 확정되지 않은 경우 empty - * @param actualModelId provider가 반환한 actual model id - * @return pricing reconciliation 판단 결과 + * @param snapshot pricing snapshot fixed before the call, or empty when it was not fixed + * @param actualModelId actual model ID returned by the provider + * @return pricing reconciliation result */ PricingReconciliationResult determineReconciliation( Optional snapshot, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingMissingListener.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingMissingListener.java index 6d23f32..8ded36c 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingMissingListener.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingMissingListener.java @@ -2,7 +2,7 @@ import io.tokenpilot.core.domain.PricingMissingEvent; -/** missing-pricing 판정을 best-effort로 관찰하는 framework-independent 계약입니다. */ +/** Framework-independent contract for best-effort observation of missing-pricing decisions. */ @FunctionalInterface public interface PricingMissingListener { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingProvider.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingProvider.java index 4edeeae..bf20b17 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingProvider.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingProvider.java @@ -4,13 +4,13 @@ import java.util.Collection; /** - * AI 모델의 가격 정책 정보를 제공하는 공급자 인터페이스. - * 로컬 파일(YAML), 데이터베이스, 또는 원격 API로부터 가격 정보를 가져올 수 있습니다. + * Provider interface for AI model pricing policies. + * Pricing data can be loaded from local files (YAML), databases, or remote APIs. */ public interface PricingProvider { /** - * 사용 가능한 모든 가격 정책 목록을 반환합니다. - * @return 가격 정책 컬렉션 + * Returns all available pricing policies. + * @return collection of pricing policies */ Collection getAllPlans(); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java index b905888..a965cc9 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/PricingRegistry.java @@ -12,38 +12,39 @@ import java.util.Optional; /** - * AI 모델별 가격 정책을 관리하는 저장소 인터페이스. + * Repository interface for managing pricing policies by AI model. */ public interface PricingRegistry { /** - * 모델 식별자로 등록된 가격 정책을 조회합니다. - * @param modelId 모델 식별자 - * @return 가격 정책 (존재하지 않을 경우 empty) + * Looks up a pricing policy registered for a model identifier. + * @param modelId model identifier + * @return pricing policy, or empty when none exists */ Optional getPlan(String modelId); /** - * 모델 식별자와 pricing policy id로 등록된 가격 정책을 조회합니다. - * @param modelId 모델 식별자 - * @param pricingPolicyId pricing policy 식별자 - * @return 가격 정책 (존재하지 않을 경우 empty) + * Looks up a pricing policy registered for a model identifier and pricing policy ID. + * @param modelId model identifier + * @param pricingPolicyId pricing policy identifier + * @return pricing policy, or empty when none exists */ Optional getPlan(String modelId, String pricingPolicyId); /** - * 모델 식별자와 pricing policy id로 요청 단위 pricing snapshot을 resolve합니다. - * @param modelId 모델 식별자 - * @param pricingPolicyId pricing policy 식별자 - * @return pricing snapshot (존재하지 않을 경우 empty) + * Resolves a request-scoped pricing snapshot by model identifier and pricing policy ID. + * @param modelId model identifier + * @param pricingPolicyId pricing policy identifier + * @return pricing snapshot, or empty when none exists */ Optional resolveSnapshot(String modelId, String pricingPolicyId); /** - * canonical model definition과 연결된 catalog version으로 pricing snapshot을 resolve합니다. - * alias는 이 메서드에서 canonical id로 변환된 뒤에만 가격 조회에 사용됩니다. + * Resolves a pricing snapshot using the catalog version associated with a + * canonical model definition. An alias is used for pricing lookup only after + * it has been converted to a canonical ID. * * @param modelDefinition canonical model metadata - * @return model definition과 연결된 pricing snapshot + * @return pricing snapshot associated with the model definition */ default Optional resolveSnapshot(ModelDefinition modelDefinition) { Objects.requireNonNull(modelDefinition, "modelDefinition must not be null"); @@ -59,11 +60,12 @@ default Optional resolveSnapshot(ModelDefinition modelDefinitio } /** - * canonical model registry를 거쳐 alias 가격 조회를 canonical 정책으로 고정합니다. + * Routes alias pricing lookup through the canonical model registry so the + * lookup is fixed to the canonical policy. * * @param modelRegistry canonical model lookup - * @param modelIdOrAlias canonical id 또는 exact alias - * @return canonical model definition과 일치하는 pricing snapshot + * @param modelIdOrAlias canonical ID or exact alias + * @return pricing snapshot matching the canonical model definition */ default Optional resolveSnapshot( ModelRegistry modelRegistry, @@ -75,25 +77,25 @@ default Optional resolveSnapshot( } /** - * 모델과 토큰 타입에 대한 가격 결정 결과를 조회합니다. - * @param modelId 모델 식별자 - * @param tokenType 토큰 타입 - * @return 가격 결정 결과 + * Resolves pricing for a model and token type. + * @param modelId model identifier + * @param tokenType token type + * @return pricing resolution */ PricingResolution resolveRate(String modelId, TokenType tokenType); /** - * 모델과 토큰 타입에 대한 가격 결정 결과를 기대 통화 기준으로 조회합니다. - * @param modelId 모델 식별자 - * @param tokenType 토큰 타입 - * @param expectedCurrency 기대 통화 - * @return 가격 결정 결과 + * Resolves pricing for a model and token type against an expected currency. + * @param modelId model identifier + * @param tokenType token type + * @param expectedCurrency expected currency + * @return pricing resolution */ PricingResolution resolveRate(String modelId, TokenType tokenType, Currency expectedCurrency); /** - * 새로운 가격 정책을 등록하거나 업데이트합니다. - * @param plan 가격 정책 + * Registers or updates a pricing policy. + * @param plan pricing policy */ void registerPlan(PricingPlan plan); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/RoundingPolicy.java b/token-pilot-core/src/main/java/io/tokenpilot/core/RoundingPolicy.java index 74ae983..549832f 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/RoundingPolicy.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/RoundingPolicy.java @@ -6,7 +6,8 @@ import java.util.Objects; /** - * 내부 정밀도를 보존한 비용을 외부 표시/청구 경계에서 반올림하는 정책입니다. + * Policy for rounding costs that preserve internal precision at an external + * display or billing boundary. */ public final class RoundingPolicy { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/TokenBudget.java b/token-pilot-core/src/main/java/io/tokenpilot/core/TokenBudget.java index 4479e8e..1ed9508 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/TokenBudget.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/TokenBudget.java @@ -4,18 +4,18 @@ import io.tokenpilot.core.domain.TokenCountResult; /** - * provider 호출 전에 입력 token과 예약 출력량의 context admission을 판단합니다. + * Determines context admission for input tokens and reserved output before provider invocation. */ public interface TokenBudget { /** - * model context window 안에 요청이 들어갈 수 있는지 fail-closed로 확인합니다. + * Fail-closed check that the request can fit within the model context window. * - * @param modelId canonical model id 또는 exact alias - * @param input 입력 token 계산 결과 - * @param reservedOutputTokens 호출 전에 확보할 최대 출력 token 수 - * @return FITS, EXCEEDS 또는 INDETERMINATE를 구분한 결과 - * @throws IllegalArgumentException reservedOutputTokens가 음수인 경우 + * @param modelId canonical model ID or exact alias + * @param input input token count result + * @param reservedOutputTokens maximum output tokens secured before the call + * @return result distinguishing FITS, EXCEEDS, and INDETERMINATE + * @throws IllegalArgumentException when reservedOutputTokens is negative */ BudgetResult check( String modelId, @@ -24,10 +24,10 @@ BudgetResult check( ); /** - * FITS가 아니면 provider 호출을 진행하지 않도록 예외를 던집니다. + * Throws an exception to prevent provider invocation when the result is not FITS. * - * @return FITS 결과 - * @throws IllegalStateException admission 결과가 FITS가 아닌 경우 + * @return FITS result + * @throws IllegalStateException when the admission result is not FITS */ default BudgetResult requireFits( String modelId, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java index 1500c44..407142b 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/TokenEstimator.java @@ -3,18 +3,19 @@ import io.tokenpilot.core.domain.TokenCountResult; /** - * LLM 호출 전에 입력의 토큰 수를 계산하는 framework-independent 계약입니다. - * 계산 결과에는 토큰 수뿐 아니라 계산 범위와 estimator/tokenization 기준이 포함됩니다. + * Framework-independent contract for counting input tokens before an LLM call. + * Results include not only the token count but also the count scope and + * estimator/tokenization basis. */ public interface TokenEstimator { /** - * 전달된 문자열의 토큰 수를 계산합니다. - * 빈 문자열은 unavailable이 아닌 0-token counted 결과로 계산해야 합니다. + * Counts tokens in the supplied string. + * An empty string must produce a counted result with zero tokens, not an unavailable result. * - * @param text 계산할 문자열 - * @return 계산값과 계산 기준을 보존한 결과 - * @throws NullPointerException text가 null인 경우 + * @param text string to count + * @return result preserving the count and counting basis + * @throws NullPointerException when text is null */ TokenCountResult estimate(String text); } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionReason.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionReason.java index 4d89c7e..62c451d 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionReason.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionReason.java @@ -1,19 +1,19 @@ package io.tokenpilot.core.domain; /** - * context admission 판정의 제한된 사유입니다. + * Enumerates the bounded reasons for a context admission decision. */ public enum AdmissionReason { - /** 입력과 예약 출력량이 context window 안에 있습니다. */ + /** The input and reserved output fit within the context window. */ WITHIN_CONTEXT, - /** 보수적 입력 상한 또는 예약 출력량이 context window를 넘습니다. */ + /** The conservative input bound or reserved output exceeds the context window. */ CONTEXT_EXCEEDED, - /** TEXT_ONLY 등 전체 요청을 포함하지 않은 결과입니다. */ + /** The result does not include the complete request, such as a TEXT_ONLY result. */ INCOMPLETE_SCOPE, - /** estimator 결과와 모델의 tokenizer 기준이 호환되지 않습니다. */ + /** The estimator result is incompatible with the model tokenizer basis. */ INCOMPATIBLE_TOKENIZER, - /** registry에 모델이 등록되어 있지 않습니다. */ + /** The model is not registered. */ UNKNOWN_MODEL, - /** token 계산 결과를 사용할 수 없습니다. */ + /** The token count result is unavailable. */ COUNT_UNAVAILABLE } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionStatus.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionStatus.java index a263d1a..782ae1c 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionStatus.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/AdmissionStatus.java @@ -1,13 +1,13 @@ package io.tokenpilot.core.domain; /** - * 입력 token과 예약 출력량의 context admission 상태입니다. + * Context admission status for input tokens and reserved output tokens. */ public enum AdmissionStatus { - /** 요청 전체가 안전한 상한 안에 있습니다. */ + /** The complete request fits within the safe upper bound. */ FITS, - /** 선언된 보수적 상한만으로 context window를 넘습니다. */ + /** The declared conservative upper bound exceeds the context window. */ EXCEEDS, - /** 안전하게 허용 또는 초과를 확정할 정보가 부족합니다. */ + /** There is not enough information to safely confirm admission or overflow. */ INDETERMINATE } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/BudgetResult.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/BudgetResult.java index fd1f995..79a701e 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/BudgetResult.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/BudgetResult.java @@ -5,10 +5,11 @@ import java.util.OptionalLong; /** - * context admission 결과와 판정에 사용한 immutable metadata입니다. + * Context admission result and immutable metadata used for the decision. * - *

{@code remainingTokens}는 FITS 결과에서만 제공됩니다. INDETERMINATE와 - * EXCEEDS에서 이를 빈 값으로 유지해 가짜 여유량을 provider 경계에 전달하지 않습니다.

+ *

{@code remainingTokens} is provided only for FITS results. It remains empty + * for INDETERMINATE and EXCEEDS so a false remaining capacity is not passed to + * the provider boundary.

*/ public record BudgetResult( AdmissionStatus status, @@ -87,9 +88,9 @@ public record BudgetResult( } /** - * FITS 상태인지 반환합니다. + * Returns whether the result has FITS status. * - * @return status가 FITS이면 true + * @return {@code true} when status is FITS */ public boolean fits() { return status == AdmissionStatus.FITS; diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java index b3be52a..4520a66 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/Cost.java @@ -5,12 +5,13 @@ import java.util.Objects; /** - * 계산된 AI 호출 비용 정보. + * Calculated AI call cost. * - * 내부 계산 정밀도를 그대로 보존하며 표시/청구를 위한 반올림은 외부 경계에서 적용합니다. + * Preserves internal calculation precision; rounding for display or billing is + * applied at an external boundary. * - * @param value 0 이상의 비용 - * @param currency 비용 통화 + * @param value non-negative cost + * @param currency cost currency */ public record Cost( BigDecimal value, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/CostRecordedEvent.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/CostRecordedEvent.java index e0e7aec..efb36a6 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/CostRecordedEvent.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/CostRecordedEvent.java @@ -3,8 +3,8 @@ import java.util.Map; /** - * 토큰 사용량과 비용이 기록되었을 때 발생하는 이벤트 데이터. - * 불변 객체(Record)로 정의하여 이벤트 전달의 안정성을 보장합니다. + * Event data emitted when token usage and cost are recorded. + * Defined as an immutable record to keep event delivery stable. */ public record CostRecordedEvent( String modelId, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/ModelDefinition.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/ModelDefinition.java index 84d8470..9af9b6a 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/ModelDefinition.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/ModelDefinition.java @@ -9,17 +9,18 @@ import java.util.Set; /** - * context admission과 pricing policy 조회에 함께 사용하는 immutable model catalog 정의입니다. + * Immutable model catalog definition used for both context admission and + * pricing policy lookup. * - * @param canonicalModelId versioned canonical model id - * @param aliases canonical id로 해석할 exact aliases - * @param encodingName 모델 encoding asset 이름 - * @param acceptedCompatibilityBasis heuristic/exact estimator가 사용할 수 있는 호환성 기준 - * @param maxContextTokens 모델 context window 크기 - * @param pricingPolicyId canonical pricing policy 식별자 - * @param catalogVersion model catalog 버전 - * @param sourceUri context/encoding metadata의 공식 출처 - * @param sourceAsOf source 확인 시점 + * @param canonicalModelId versioned canonical model ID + * @param aliases exact aliases resolved to the canonical ID + * @param encodingName model encoding asset name + * @param acceptedCompatibilityBasis compatibility basis accepted by heuristic/exact estimators + * @param maxContextTokens model context window size + * @param pricingPolicyId canonical pricing policy identifier + * @param catalogVersion model catalog version + * @param sourceUri authoritative source for context/encoding metadata + * @param sourceAsOf time at which the source was checked */ public record ModelDefinition( String canonicalModelId, @@ -59,7 +60,8 @@ public record ModelDefinition( } /** - * 기존 모델 정의 생성 코드와의 호환성을 위해 USD를 기본 통화로 사용합니다. + * Uses USD as the default currency for compatibility with existing model + * definition construction code. */ public ModelDefinition( String canonicalModelId, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostResult.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostResult.java index d6fcf78..0db4fb1 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostResult.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostResult.java @@ -4,28 +4,28 @@ import java.util.Objects; /** - * 호출 전 비용 계산 결과입니다. 숫자 상한이 있는 결과와 계산 불가 결과를 - * sealed hierarchy로 분리해 0원과 pricing 부재를 혼동하지 않습니다. + * Result of preflight cost calculation. A sealed hierarchy separates numeric + * bounds from unavailable results so zero cost is not confused with missing pricing. */ public sealed interface PreflightCostResult permits PreflightCostResult.Bounded, PreflightCostResult.Unavailable { /** - * 하나의 불변 pricing snapshot으로 계산한 유한 비용 상한입니다. - * 내부 계산에서는 반올림하지 않으며, context admission을 통과했다는 의미는 아닙니다. - * 예약 계층은 반드시 {@code safeUpperBoundCost}를 사용해야 합니다. + * Finite cost bound calculated from one immutable pricing snapshot. + * Internal calculations are not rounded, and this does not imply that context admission passed. + * The reservation layer must use {@code safeUpperBoundCost}. * - * @param estimatedCost 관찰과 표시를 위한 예상 비용 - * @param safeUpperBoundCost 예약에 사용할 보수적인 비용 상한 - * @param inputEstimatedTokens 정보 제공용 입력 token 계산값 - * @param inputSafeUpperBoundTokens 보수적인 입력 token 안전 상한 - * @param reservedOutputTokens 예약한 최대 출력 token 수 - * @param canonicalModelId canonical model 식별자 - * @param pricingPolicyId 불변 pricing policy 식별자 - * @param catalogVersion 계산에 사용한 catalog 버전 - * @param pricingSnapshot 계산·예약·정산에 전달할 정확한 가격 snapshot - * @param estimatorDescriptor token estimator 식별 정보 - * @param tokenizationBasis 검증된 tokenizer 호환성 기준 + * @param estimatedCost estimated cost for observation and display + * @param safeUpperBoundCost conservative cost bound used for reservation + * @param inputEstimatedTokens informational input token count + * @param inputSafeUpperBoundTokens safe input token bound for conservative admission + * @param reservedOutputTokens maximum output tokens reserved + * @param canonicalModelId canonical model identifier + * @param pricingPolicyId immutable pricing policy identifier + * @param catalogVersion catalog version used for calculation + * @param pricingSnapshot exact pricing snapshot passed to calculation, reservation, and settlement + * @param estimatorDescriptor token estimator identity + * @param tokenizationBasis validated tokenizer compatibility basis */ record Bounded( Cost estimatedCost, @@ -42,7 +42,7 @@ record Bounded( ) implements PreflightCostResult { /** - * 비용, token 관계와 재현성 metadata를 검증합니다. + * Validates cost/token relationships and reproducibility metadata. */ public Bounded { estimatedCost = Objects.requireNonNull(estimatedCost, "estimatedCost must not be null"); @@ -78,9 +78,9 @@ record Bounded( } /** - * 계산된 비용의 통화를 반환합니다. + * Returns the currency of the calculated costs. * - * @return estimated cost와 safe upper bound cost가 공유하는 통화 + * @return the currency shared by the estimated and safe upper-bound costs */ public Currency currency() { return safeUpperBoundCost.currency(); @@ -88,18 +88,19 @@ public Currency currency() { } /** - * fail-closed 경계에서 숫자 비용으로 취급할 수 없는 결과입니다. - * 이 결과는 0원이나 임의의 기본 단가를 의미하지 않으며, provider 호출과 예약을 - * 계속할지 여부는 각 control 계층이 사유를 확인해 결정해야 합니다. + * Result that cannot be treated as a numeric cost at a fail-closed boundary. + * It does not mean zero cost or an arbitrary default rate. Each control layer + * must inspect the reason before deciding whether to continue with provider + * invocation and reservation. * - * @param reason 숫자 비용을 생성하지 못한 제한된 사유 - * @param canonicalModelId canonical model 식별자 - * @param pricingPolicyId 참조한 pricing policy 식별자 - * @param catalogVersion 참조한 catalog 버전 - * @param currency 모델이 요구한 비용 통화 - * @param reservedOutputTokens 요청한 최대 출력 token 수 - * @param estimatorDescriptor token estimator 식별 정보 - * @param tokenizationBasis token 결과의 tokenizer 호환성 기준 + * @param reason bounded reason a numeric cost could not be produced + * @param canonicalModelId canonical model identifier + * @param pricingPolicyId referenced pricing policy identifier + * @param catalogVersion referenced catalog version + * @param currency cost currency required by the model + * @param reservedOutputTokens requested maximum output token count + * @param estimatorDescriptor token estimator identity + * @param tokenizationBasis tokenizer compatibility basis of the token result */ record Unavailable( PreflightCostUnavailableReason reason, @@ -113,7 +114,7 @@ record Unavailable( ) implements PreflightCostResult { /** - * Unavailable 사유와 진단 metadata를 검증합니다. + * Validates the unavailable reason and diagnostic metadata. */ public Unavailable { reason = Objects.requireNonNull(reason, "reason must not be null"); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostUnavailableReason.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostUnavailableReason.java index 7a08569..fd13804 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostUnavailableReason.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightCostUnavailableReason.java @@ -1,25 +1,26 @@ package io.tokenpilot.core.domain; /** - * 호출 전 보수적 비용 상한을 숫자로 확정할 수 없는 제한된 사유입니다. + * Enumerates the bounded reasons a conservative preflight cost bound cannot be + * established as a number. */ public enum PreflightCostUnavailableReason { - /** token 계산 결과를 사용할 수 없는 상태입니다. */ + /** The token count result is unavailable. */ COUNT_UNAVAILABLE, - /** 전체 REQUEST 범위를 포함하지 않은 token 결과입니다. */ + /** The token result does not cover the complete REQUEST scope. */ INCOMPLETE_SCOPE, - /** estimator와 model의 tokenizer 기준이 호환되지 않습니다. */ + /** The estimator and model tokenizer bases are incompatible. */ INCOMPATIBLE_TOKENIZER, - /** 참조한 pricing policy snapshot을 찾지 못했습니다. */ + /** The referenced pricing policy snapshot was not found. */ PRICING_NOT_FOUND, - /** 비용 계산에 필요한 기본 입력/출력 단가가 누락됐습니다. */ + /** A required base input or output rate is missing. */ INCOMPLETE_PRICING, - /** pricing policy가 호출 전에 유한한 상한을 제공할 수 없습니다. */ + /** The pricing policy cannot provide a finite bound before the call. */ UNBOUNDED_PRICING, - /** model과 pricing snapshot의 통화가 다릅니다. */ + /** The model and pricing snapshot use different currencies. */ CURRENCY_MISMATCH, - /** canonical model, policy 또는 catalog 식별자가 snapshot과 다릅니다. */ + /** The canonical model, policy, or catalog identifier differs from the snapshot. */ PRICING_SNAPSHOT_MISMATCH, - /** 가격 정밀도가 계산 범위를 벗어나 정형 비용 결과를 만들 수 없습니다. */ + /** Price precision exceeded the calculation range, so a structured cost result could not be created. */ ARITHMETIC_FAILURE } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightDecisionEvent.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightDecisionEvent.java index efd3eea..cd1313d 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightDecisionEvent.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightDecisionEvent.java @@ -3,9 +3,9 @@ import java.util.Objects; /** - * provider 호출 전 context admission 판정을 전달하는 framework-independent 이벤트입니다. + * Framework-independent event carrying a context admission decision before provider invocation. * - * @param result bounded status/reason과 판정 metadata를 포함한 immutable 결과 + * @param result immutable result containing bounded status/reason and decision metadata */ public record PreflightDecisionEvent(BudgetResult result) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightPricingContext.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightPricingContext.java index 42a6541..e84d7e9 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightPricingContext.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PreflightPricingContext.java @@ -5,27 +5,29 @@ import java.util.Optional; /** - * canonical model 해석 이후 preflight 비용 계산에 전달하는 불변 pricing projection입니다. + * Immutable pricing projection passed to preflight cost calculation after the + * canonical model has been resolved. * - *

alias는 ModelRegistry에서 먼저 canonical model로 해석해야 하며, 이 문맥을 - * 임의의 문자열 조합으로 만들면 안 됩니다. {@code catalogVersion}, - * {@code pricingPolicyId}, 통화와 tokenizer 기준은 같은 모델 정의와 pricing - * snapshot에서 함께 파생되어야 합니다.

+ *

An alias must first be resolved to a canonical model by ModelRegistry; this + * context must not be assembled from arbitrary strings. {@code catalogVersion}, + * {@code pricingPolicyId}, currency, and tokenizer basis must be derived + * together from the same model definition and pricing snapshot.

* - *

계산 가능한 문맥은 registry에서 한 번 확정한 동일한 불변 snapshot을 함께 - * 보관해야 합니다. 계산·예약·정산 과정에서 mutable registry를 다시 조회하면 - * 서로 다른 가격을 섞을 수 있습니다. 가격을 찾지 못한 문맥은 빈 snapshot으로 - * 만들 수 있으며, 계산기는 이를 {@code PRICING_NOT_FOUND}로 반환합니다. - * {@link UpperBoundCapability} 역시 호출자가 임의로 선언하는 값이 아니라 검증된 - * pricing policy의 결과여야 합니다.

+ *

A calculable context must retain the same immutable snapshot resolved once + * from the registry. Looking up a mutable registry again during calculation, + * reservation, or settlement could mix prices from different observations. A + * context with no pricing can use an empty snapshot, which the calculator + * returns as {@code PRICING_NOT_FOUND}. {@link UpperBoundCapability} must also + * be the result of a validated pricing policy rather than an arbitrary caller + * declaration.

* - * @param canonicalModelId canonical model 식별자 - * @param pricingPolicyId 불변 pricing policy 식별자 - * @param catalogVersion model과 pricing catalog의 버전 - * @param tokenizationBasis 모델이 허용하는 tokenizer 호환성 기준 - * @param currency 모델이 요구하는 비용 통화 - * @param upperBoundCapability pricing policy가 유한한 비용 상한을 제공할 수 있는지 나타내는 결과 - * @param pricingSnapshot 이 계산·예약·정산에 사용할 확정 가격 snapshot + * @param canonicalModelId canonical model identifier + * @param pricingPolicyId immutable pricing policy identifier + * @param catalogVersion model and pricing catalog version + * @param tokenizationBasis tokenizer compatibility basis accepted by the model + * @param currency cost currency required by the model + * @param upperBoundCapability result indicating whether the pricing policy can provide a finite cost bound + * @param pricingSnapshot resolved pricing snapshot used for calculation, reservation, and settlement */ public record PreflightPricingContext( String canonicalModelId, @@ -37,7 +39,7 @@ public record PreflightPricingContext( Optional pricingSnapshot ) { - /** 모든 식별자와 호환성 metadata가 비어 있지 않은지 검증합니다. */ + /** Validates that identifiers and compatibility metadata are not blank. */ public PreflightPricingContext { canonicalModelId = requireText(canonicalModelId, "canonicalModelId"); pricingPolicyId = requireText(pricingPolicyId, "pricingPolicyId"); @@ -52,15 +54,15 @@ public record PreflightPricingContext( } /** - * 아직 가격 snapshot을 찾지 못한 문맥을 생성합니다. - * 계산기는 이 문맥을 숫자 비용이 아닌 {@code PRICING_NOT_FOUND}로 처리합니다. + * Creates a context for which a pricing snapshot has not yet been found. + * The calculator handles this context as {@code PRICING_NOT_FOUND}, not as a numeric cost. * - * @param canonicalModelId canonical model 식별자 - * @param pricingPolicyId pricing policy 식별자 - * @param catalogVersion model과 pricing catalog의 버전 - * @param tokenizationBasis tokenizer 호환성 기준 - * @param currency 비용 통화 - * @param upperBoundCapability 가격 정책의 상한 제공 가능 여부 + * @param canonicalModelId canonical model identifier + * @param pricingPolicyId pricing policy identifier + * @param catalogVersion model and pricing catalog version + * @param tokenizationBasis tokenizer compatibility basis + * @param currency cost currency + * @param upperBoundCapability whether the pricing policy can provide a cost bound */ public PreflightPricingContext( String canonicalModelId, @@ -88,11 +90,11 @@ private static String requireText(String value, String name) { return value; } - /** pricing policy가 호출 전에 유한한 보수적 상한을 제공할 수 있는지 나타냅니다. */ + /** Describes whether the pricing policy can provide a finite conservative bound before a call. */ public enum UpperBoundCapability { - /** 모든 적용 경로에 유한한 최대 단가가 있습니다. */ + /** Every applicable path has a finite maximum rate. */ FINITE, - /** 하나 이상의 적용 경로에 유한한 최대 단가가 없습니다. */ + /** At least one applicable path has no finite maximum rate. */ UNBOUNDED } } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingMissingEvent.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingMissingEvent.java index 94580f1..dc78e55 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingMissingEvent.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingMissingEvent.java @@ -3,10 +3,10 @@ import java.util.Objects; /** - * 명시적인 missing-pricing 판정을 전달하는 framework-independent 이벤트입니다. + * Framework-independent event carrying an explicit missing-pricing decision. * - * @param policy missing pricing 처리 정책 - * @param resolution missing plan/rate 판정 + * @param policy policy for handling missing pricing + * @param resolution missing-plan or missing-rate decision */ public record PricingMissingEvent( MissingPricingPolicy policy, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java index 9de0d72..d14e1f0 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingPlan.java @@ -8,12 +8,12 @@ import java.util.Objects; /** - * 특정 모델의 가격 정책 정보. - * {@link TokenType} 별로 1,000(1K) 토큰당 가격을 관리합니다. + * Pricing policy information for a specific model. + * Maintains the price per 1,000 (1K) tokens for each {@link TokenType}. * - * @param modelId 모델 식별자 (예: gpt-4o, claude-3-5-sonnet) - * @param rates 1K 토큰 타입별 단가 (Map) - * @param currency 통화 (기본값: USD) + * @param modelId model identifier, such as gpt-4o or claude-3-5-sonnet + * @param rates per-1K-token rate by token type + * @param currency currency, defaulting to USD */ public record PricingPlan( String modelId, @@ -48,21 +48,21 @@ public PricingPlan(String modelId, Map rates, Currency cu } /** - * 기본 입력/출력 단가와 통화를 사용하는 {@link PricingPlan}을 생성합니다. + * Creates a {@link PricingPlan} with basic input/output rates and a currency. */ public PricingPlan(String modelId, BigDecimal promptPricePerK, BigDecimal completionPricePerK, Currency currency) { this(modelId, DEFAULT_PRICING_POLICY_ID, createRates(promptPricePerK, completionPricePerK), currency); } /** - * 기본 입력/출력 단가와 pricing policy id, 통화를 사용하는 {@link PricingPlan}을 생성합니다. + * Creates a {@link PricingPlan} with basic input/output rates, a pricing policy ID, and a currency. */ public PricingPlan(String modelId, String pricingPolicyId, BigDecimal promptPricePerK, BigDecimal completionPricePerK, Currency currency) { this(modelId, pricingPolicyId, createRates(promptPricePerK, completionPricePerK), currency); } /** - * 기본 입력/출력 단가만 사용하는 {@link PricingPlan}을 생성합니다. 기본 통화는 USD입니다. + * Creates a {@link PricingPlan} with basic input/output rates and the default USD currency. */ public PricingPlan(String modelId, BigDecimal promptPricePerK, BigDecimal completionPricePerK) { this(modelId, promptPricePerK, completionPricePerK, Currency.getInstance("USD")); @@ -76,21 +76,22 @@ private static Map createRates(BigDecimal prompt, BigDeci } /** - * 기본 입력 단가를 반환합니다. (하위 호환성) + * Returns the basic input rate for compatibility. */ public BigDecimal promptPricePerK() { return getRate(TokenType.PROMPT); } /** - * 기본 출력 단가를 반환합니다. (하위 호환성) + * Returns the basic output rate for compatibility. */ public BigDecimal completionPricePerK() { return getRate(TokenType.COMPLETION); } /** - * 특정 토큰 타입의 단가를 가져옵니다. 없을 시 계층 구조에 따라 대체값을 반환합니다. + * Returns the rate for a token type, falling back through the rate hierarchy + * when a direct rate is absent. * REASONING -> COMPLETION * CACHE_READ_PROMPT, CACHE_CREATION_PROMPT -> PROMPT */ @@ -105,9 +106,10 @@ public BigDecimal getRate(TokenType type) { } /** - * 특정 토큰 타입의 가격 결정 결과를 반환합니다. - * 명시적으로 등록된 0 rate는 {@link PricingResolution#RESOLVED}로, - * 누락된 rate는 {@link PricingResolution#MISSING_RATE}로 표현합니다. + * Returns the pricing resolution for a token type. + * An explicitly registered zero rate is represented as + * {@link PricingResolution#RESOLVED}; a missing rate is represented as + * {@link PricingResolution#MISSING_RATE}. */ public PricingResolution resolveRate(TokenType type) { if (rates.containsKey(type)) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.java index 8964bd1..5df9e75 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingReconciliationResult.java @@ -1,7 +1,7 @@ package io.tokenpilot.core.domain; /** - * Actual reconciliation 결과. + * Actual reconciliation result. */ public enum PricingReconciliationResult { RECONCILED, diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java index a945d1b..5cc20b1 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/PricingSnapshot.java @@ -9,7 +9,7 @@ import java.util.Objects; /** - * provider 호출 전에 확정된 요청 단위 pricing snapshot. + * Request-scoped pricing snapshot resolved before provider invocation. */ public record PricingSnapshot( String modelId, @@ -57,17 +57,17 @@ public static PricingSnapshot from(PricingPlan plan, String catalogVersion, Inst } /** - * 가격 조회 시각을 제외하고 실제 비용 책임을 결정하는 terms가 같은지 반환합니다. - * {@link #checkedAt()}은 같은 요청의 재시도에서 달라질 수 있는 관측 metadata이므로 - * 멱등성 fingerprint에는 포함하지 않습니다. + * Returns whether the billing terms are equal, excluding the pricing lookup time. + * {@link #checkedAt()} is observational metadata that may differ on retries of + * the same request, so it is excluded from the idempotency fingerprint. */ public boolean hasSameBillingTerms(PricingSnapshot other) { return haveSameBillingTerms(this, other); } /** - * 두 snapshot의 비용 책임 terms를 비교합니다. 테스트 double처럼 rate map이 없는 - * 호환 snapshot은 양쪽 모두 없을 때 같은 것으로 취급합니다. + * Compares the billing terms of two snapshots. Compatible snapshots without a + * rate map, such as test doubles, are considered equal when both maps are absent. */ public static boolean haveSameBillingTerms( PricingSnapshot first, @@ -95,7 +95,7 @@ public static boolean haveSameBillingTerms( } /** - * {@link #hasSameBillingTerms(PricingSnapshot)}와 일치하는 bounded hash code입니다. + * Bounded hash code consistent with {@link #hasSameBillingTerms(PricingSnapshot)}. */ public int billingTermsHashCode() { int result = Objects.hash(modelId, pricingPolicyId, catalogVersion, currency); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountAccuracy.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountAccuracy.java index 827996c..fe625d7 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountAccuracy.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountAccuracy.java @@ -1,16 +1,16 @@ package io.tokenpilot.core.domain; /** - * counted token 결과의 계산 정확도를 나타냅니다. + * Describes the accuracy of a counted token result. */ public enum TokenCountAccuracy { /** - * 계산값이 정확하며 tokens와 safeUpperBoundTokens가 동일한 상태입니다. + * The count is exact, so tokens and safeUpperBoundTokens are equal. */ EXACT, /** - * 계산값이 추정치이며 safeUpperBoundTokens가 tokens 이상인 상태입니다. + * The count is estimated, so safeUpperBoundTokens is greater than or equal to tokens. */ HEURISTIC } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java index 40479bc..b5bf454 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountResult.java @@ -5,8 +5,8 @@ import java.util.OptionalLong; /** - * LLM 호출 전에 수행한 token 계산의 immutable snapshot입니다. - * 계산된 결과와 계산할 수 없는 결과를 상호 배타적으로 표현합니다. + * Immutable snapshot of a token count performed before an LLM call. + * Represents counted and unavailable results as mutually exclusive states. */ public final class TokenCountResult { private final ResultState state; @@ -27,17 +27,17 @@ private TokenCountResult( } /** - * token 계산값이 존재하는 counted 결과를 생성합니다. + * Creates a counted result containing a token count. * - * @param tokens 정보 제공용 token 계산값 - * @param safeUpperBoundTokens 보수적 admission 판단에 사용할 안전 상한 - * @param accuracy 계산 정확도 - * @param scope 계산에 포함된 입력 범위 - * @param estimatorDescriptor 계산에 사용된 estimator 식별 정보 - * @param tokenizationBasis 모델 encoding과 비교할 tokenization 기준 - * @return 검증된 counted 결과 - * @throws IllegalArgumentException token 값이나 accuracy별 상한 관계가 유효하지 않은 경우 - * @throws NullPointerException accuracy, scope, estimatorDescriptor 또는 tokenizationBasis가 null인 경우 + * @param tokens informational token count + * @param safeUpperBoundTokens safe upper bound for conservative admission decisions + * @param accuracy counting accuracy + * @param scope input scope included in the count + * @param estimatorDescriptor estimator identity used for counting + * @param tokenizationBasis tokenization basis compared with the model encoding + * @return a validated counted result + * @throws IllegalArgumentException when token values or the accuracy-specific bound relationship is invalid + * @throws NullPointerException when accuracy, scope, estimatorDescriptor, or tokenizationBasis is null */ public static TokenCountResult counted( long tokens, @@ -96,14 +96,14 @@ private static void validateCounted( } /** - * token 계산 불가 결과를 생성합니다. + * Creates an unavailable token count result. * - * @param reason 계산 결과를 제공하지 못한 제한된 사유 - * @param scope 계산하려고 한 입력 범위 - * @param estimatorDescriptor 계산을 시도한 estimator 식별 정보 - * @param tokenizationBasis estimator가 선언한 tokenization 기준 - * @return unavailable 결과 - * @throws NullPointerException 인자가 null인 경우 + * @param reason bounded reason the count could not be provided + * @param scope input scope that was requested + * @param estimatorDescriptor estimator identity used for the attempt + * @param tokenizationBasis tokenization basis declared by the estimator + * @return an unavailable result + * @throws NullPointerException when an argument is null */ public static TokenCountResult unavailable( TokenCountUnavailableReason reason, @@ -125,18 +125,18 @@ public static TokenCountResult unavailable( } /** - * token 계산값이 존재하는 counted 상태인지 확인합니다. + * Returns whether this result is a counted state containing a token value. * - * @return counted 상태이면 true + * @return {@code true} when this is a counted state */ public boolean isCounted() { return state instanceof Counted; } /** - * counted 결과가 정확한 계산값인지 확인합니다. + * Returns whether the counted result is exact. * - * @return accuracy가 EXACT이면 true, HEURISTIC 또는 unavailable이면 false + * @return {@code true} when accuracy is EXACT; {@code false} for HEURISTIC or unavailable */ public boolean isExact() { if (state instanceof Counted counted) { @@ -146,18 +146,18 @@ public boolean isExact() { } /** - * token 계산값을 제공하지 못한 unavailable 상태인지 확인합니다. + * Returns whether the token count is unavailable. * - * @return unavailable 상태이면 true + * @return {@code true} when this is an unavailable state */ public boolean isUnavailable() { return state instanceof Unavailable; } /** - * 정보 제공용 token 계산값을 반환합니다. + * Returns the informational token count. * - * @return counted 상태의 계산값, unavailable 상태이면 empty + * @return the counted value, or empty for an unavailable state */ public OptionalLong tokens() { if (state instanceof Counted counted) { @@ -167,9 +167,9 @@ public OptionalLong tokens() { } /** - * 보수적 admission 판단에 사용할 안전 상한을 반환합니다. + * Returns the safe upper bound used for conservative admission decisions. * - * @return counted 상태의 안전 상한, unavailable 상태이면 empty + * @return the counted safe upper bound, or empty for an unavailable state */ public OptionalLong safeUpperBoundTokens() { if (state instanceof Counted counted) { @@ -179,9 +179,9 @@ public OptionalLong safeUpperBoundTokens() { } /** - * counted 결과의 계산 정확도를 반환합니다. + * Returns the counting accuracy of a counted result. * - * @return counted 상태의 정확도, unavailable 상태이면 empty + * @return the counted accuracy, or empty for an unavailable state */ public Optional accuracy() { if (state instanceof Counted counted) { @@ -191,9 +191,9 @@ public Optional accuracy() { } /** - * token 계산 결과를 제공하지 못한 사유를 반환합니다. + * Returns the reason the token count could not be provided. * - * @return unavailable 상태의 사유, counted 상태이면 empty + * @return the unavailable reason, or empty for a counted state */ public Optional unavailableReason() { if (state instanceof Unavailable(TokenCountUnavailableReason reason)) { @@ -203,27 +203,27 @@ public Optional unavailableReason() { } /** - * token 계산에 포함된 입력 범위를 반환합니다. + * Returns the input scope included in the token count. * - * @return 생성 시점의 계산 범위 + * @return the scope at creation time */ public TokenCountScope scope() { return scope; } /** - * token 계산에 사용된 estimator 식별 정보를 반환합니다. + * Returns the estimator identity used for token counting. * - * @return 생성 시점의 estimator 식별 정보 + * @return the estimator identity at creation time */ public TokenEstimatorDescriptor estimatorDescriptor() { return estimatorDescriptor; } /** - * 모델 encoding과 비교할 tokenization 기준을 반환합니다. + * Returns the tokenization basis to compare with the model encoding. * - * @return 생성 시점의 tokenization 기준 + * @return the tokenization basis at creation time */ public TokenizationBasis tokenizationBasis() { return tokenizationBasis; diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountScope.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountScope.java index 600ed30..f18523e 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountScope.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountScope.java @@ -1,19 +1,22 @@ package io.tokenpilot.core.domain; /** - * token 계산에 포함된 입력 범위를 나타냅니다. - * 기존 결과의 scope만 변경할 수 없으며, 더 넓은 범위를 계산했다면 새로운 결과를 생성해야 합니다. + * Describes the input scope included in a token count. + * An existing result cannot be relabeled with a different scope; a new result + * must be created when a broader scope has been counted. */ public enum TokenCountScope { /** - * 전달된 문자열의 UTF-8 내용만 계산한 범위입니다. - * role, tool schema, media metadata, structured-output schema와 provider framing은 포함하지 않으므로 - * 전체 요청이 context window에 들어간다는 근거로 사용할 수 없습니다. + * Counts only the UTF-8 content of the supplied strings. + * It excludes roles, tool schemas, media metadata, structured-output schemas, + * and provider framing, so it cannot prove that the complete request fits in + * the context window. */ TEXT_ONLY, /** - * 실제 전송 요청의 tokenizable content와 필요한 framing/headroom을 모두 포함한 범위입니다. + * Includes all tokenizable content and required framing/headroom for the + * request that will actually be sent. */ REQUEST } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountUnavailableReason.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountUnavailableReason.java index 59e71d3..32c44de 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountUnavailableReason.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenCountUnavailableReason.java @@ -1,11 +1,11 @@ package io.tokenpilot.core.domain; /** - * token estimator가 계산 결과를 제공하지 못한 이유를 제한된 값으로 나타냅니다. + * Enumerates the bounded reasons a token estimator cannot provide a result. */ public enum TokenCountUnavailableReason { /** - * 설정된 estimator가 token 계산 결과를 제공할 수 없는 상태입니다. + * The configured estimator cannot provide a token count result. */ ESTIMATOR_UNAVAILABLE } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenEstimatorDescriptor.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenEstimatorDescriptor.java index 996c549..1f61d75 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenEstimatorDescriptor.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenEstimatorDescriptor.java @@ -1,10 +1,10 @@ package io.tokenpilot.core.domain; /** - * token 계산에 사용된 estimator 구현과 버전을 식별합니다. + * Identifies the estimator implementation and version used for token counting. * - * @param estimatorId 안정적인 estimator 구현 식별자 - * @param estimatorVersion 계산식이나 asset 변경을 구분하는 버전 + * @param estimatorId stable estimator implementation identifier + * @param estimatorVersion version distinguishing formula or asset changes */ public record TokenEstimatorDescriptor( String estimatorId, @@ -12,9 +12,9 @@ public record TokenEstimatorDescriptor( ) { /** - * estimator 식별자와 버전이 null 또는 blank가 아닌지 검증합니다. + * Validates that the estimator identifier and version are not null or blank. * - * @throws IllegalArgumentException estimatorId 또는 estimatorVersion이 null이거나 blank인 경우 + * @throws IllegalArgumentException when estimatorId or estimatorVersion is null or blank */ public TokenEstimatorDescriptor { estimatorId = requireText(estimatorId, "estimatorId"); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenType.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenType.java index 94ddd9b..d79e52d 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenType.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenType.java @@ -1,33 +1,33 @@ package io.tokenpilot.core.domain; /** - * AI 모델 호출 시 발생하는 토큰의 세부 유형. + * Detailed token types produced by an AI model call. */ public enum TokenType { - /** 일반 입력 (Prompt) */ + /** Regular input (prompt). */ PROMPT, - /** 일반 출력 (Completion) */ + /** Regular output (completion). */ COMPLETION, - /** 추론 (Reasoning) - 주로 출력 계열 */ + /** Reasoning tokens, usually part of the output category. */ REASONING, - /** 캐시에서 읽은 입력 (Cache Read Prompt) */ + /** Input read from the cache (cache-read prompt). */ CACHE_READ_PROMPT, - /** 캐시에 새로 저장한 입력 (Cache Creation Prompt) */ + /** Input newly stored in the cache (cache-creation prompt). */ CACHE_CREATION_PROMPT; /** - * 해당 토큰 타입이 입력(Prompt) 계열인지 확인합니다. + * Returns whether this token type belongs to the input (prompt) category. * - * @return 입력 계열이면 {@code true} + * @return {@code true} when this is an input category */ public boolean isPrompt() { return this == PROMPT || this == CACHE_READ_PROMPT || this == CACHE_CREATION_PROMPT; } /** - * 해당 토큰 타입이 출력(Completion) 계열인지 확인합니다. + * Returns whether this token type belongs to the output (completion) category. * - * @return 출력 계열이면 {@code true} + * @return {@code true} when this is an output category */ public boolean isCompletion() { return this == COMPLETION || this == REASONING; diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsage.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsage.java index 6c82eaf..1fa03ed 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsage.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsage.java @@ -4,14 +4,14 @@ import java.util.Objects; /** - * AI 모델 호출 시 발생하는 토큰 사용량 정보. - * 전체 입력/출력 토큰과 세부 사용량을 관리합니다. + * Token usage information produced by an AI model call. + * Manages inclusive input/output totals and usage details. * - * @param inputTokens 전체 입력 토큰 - * @param outputTokens 전체 출력 토큰 - * @param details 세부 토큰 사용량 - * @param source 사용량 값의 출처 - * @param metadata 추가 메타데이터 (예: 모델 정보 등) + * @param inputTokens total input tokens + * @param outputTokens total output tokens + * @param details detailed token usage + * @param source source of the usage values + * @param metadata additional metadata, such as model information */ public record TokenUsage( long inputTokens, @@ -21,10 +21,11 @@ public record TokenUsage( Map metadata ) { /** - * 토큰 총량과 세부량의 포함 관계를 검증하고 metadata를 불변 복사합니다. + * Validates containment between token totals and details, then makes an + * immutable copy of metadata. * - * @throws IllegalArgumentException 토큰 수가 음수이거나 세부량이 총량을 초과한 경우 - * @throws NullPointerException details/source가 null이거나 metadata에 null key/value가 있는 경우 + * @throws IllegalArgumentException when token counts are negative or details exceed their totals + * @throws NullPointerException when details/source is null or metadata contains a null key/value */ public TokenUsage { if (inputTokens < 0) { @@ -68,11 +69,11 @@ public record TokenUsage( } /** - * 기본 입력/출력 토큰을 사용하는 {@link TokenUsage}를 생성합니다. + * Creates {@link TokenUsage} with basic input and output token totals. * - * @param prompt 전체 입력 토큰 - * @param completion 전체 출력 토큰 - * @return 세부량과 metadata가 비어 있는 사용량 + * @param prompt total input tokens + * @param completion total output tokens + * @return usage with no details or metadata */ public static TokenUsage from(long prompt, long completion) { return new TokenUsage( @@ -85,12 +86,12 @@ public static TokenUsage from(long prompt, long completion) { } /** - * 입력/출력/추론 토큰을 포함하는 {@link TokenUsage}를 생성합니다. + * Creates {@link TokenUsage} with input, output, and reasoning tokens. * - * @param prompt 전체 입력 토큰 - * @param completion 전체 출력 토큰 - * @param reasoning 전체 출력에 포함된 reasoning 토큰 - * @return reasoning 세부량을 포함하는 사용량 + * @param prompt total input tokens + * @param completion total output tokens + * @param reasoning reasoning tokens included in total output + * @return usage containing reasoning details */ public static TokenUsage from(long prompt, long completion, long reasoning) { return new TokenUsage( @@ -103,10 +104,10 @@ public static TokenUsage from(long prompt, long completion, long reasoning) { } /** - * provider 응답에서 사용량 정보를 얻지 못한 상태를 생성합니다. + * Creates a state in which usage information was unavailable in the provider response. * - * @param metadata 보존할 응답 메타데이터 - * @return 출처가 {@link UsageSource#UNAVAILABLE}인 0 토큰 사용량 + * @param metadata response metadata to preserve + * @return zero-token usage with source {@link UsageSource#UNAVAILABLE} */ public static TokenUsage unavailable(Map metadata) { return new TokenUsage( @@ -119,40 +120,41 @@ public static TokenUsage unavailable(Map metadata) { } /** - * 모든 종류의 입력/출력 토큰 수의 합계를 반환합니다. + * Returns the total number of input tokens across all input categories. * - * @return 전체 입력 토큰 + * @return total input tokens */ public long promptTokens() { return inputTokens; } /** - * 모든 종류의 출력(추론 포함) 토큰 수의 합계를 반환합니다. + * Returns the total number of output tokens, including reasoning tokens. * - * @return 전체 출력 토큰 + * @return total output tokens */ public long completionTokens() { return outputTokens; } /** - * 전체 사용 토큰 수의 합계를 반환합니다. + * Returns the total number of used tokens. * - * @return 전체 입력과 출력 토큰의 합 - * @throws ArithmeticException 합계가 {@code long} 범위를 초과한 경우 + * @return the sum of total input and output tokens + * @throws ArithmeticException when the sum exceeds the {@code long} range */ public long totalTokens() { return Math.addExact(inputTokens, outputTokens); } /** - * 특정 토큰 타입의 전체 또는 세부 사용량을 반환합니다. - * 보고되지 않은 세부량은 이 호환 projection에서 {@code 0}으로 반환되며, - * 미보고 여부는 {@link #details()}의 nullable 필드로 확인해야 합니다. + * Returns the total or detailed usage for a token type. + * Unreported details are returned as {@code 0} in this compatibility + * projection; use the nullable fields in {@link #details()} to distinguish + * unreported values. * - * @param type 조회할 토큰 타입 - * @return 해당 토큰 타입의 사용량 + * @param type token type to retrieve + * @return usage for the requested token type */ public long getCount(TokenType type) { return switch (type) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsageDetails.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsageDetails.java index 8c467ae..ed30320 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsageDetails.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenUsageDetails.java @@ -1,13 +1,13 @@ package io.tokenpilot.core.domain; /** - * 정규화된 전체 입력/출력 토큰에 포함되는 선택적 세부 사용량. - * {@code null}은 provider가 값을 보고하지 않았음을, {@code 0}은 값을 - * 보고했으나 실제 사용량이 없었음을 뜻합니다. + * Optional usage details included in normalized inclusive input/output totals. + * {@code null} means the provider did not report a value, while {@code 0} + * means it reported the value but no tokens were used. * - * @param cacheReadInputTokens 전체 입력에 포함된 cache read 토큰 또는 미보고 시 {@code null} - * @param cacheCreationInputTokens 전체 입력에 포함된 cache creation 토큰 또는 미보고 시 {@code null} - * @param reasoningOutputTokens 전체 출력에 포함된 reasoning 토큰 또는 미보고 시 {@code null} + * @param cacheReadInputTokens cache-read tokens included in total input, or {@code null} when unreported + * @param cacheCreationInputTokens cache-creation tokens included in total input, or {@code null} when unreported + * @param reasoningOutputTokens reasoning tokens included in total output, or {@code null} when unreported */ public record TokenUsageDetails( Long cacheReadInputTokens, @@ -15,9 +15,9 @@ public record TokenUsageDetails( Long reasoningOutputTokens ) { /** - * 모든 세부 토큰 수가 0 이상인지 검증합니다. + * Validates that every detail token count is non-negative. * - * @throws IllegalArgumentException 세부 토큰 수가 음수인 경우 + * @throws IllegalArgumentException when a detail token count is negative */ public TokenUsageDetails { isEmptyToken(cacheReadInputTokens); @@ -26,9 +26,9 @@ public record TokenUsageDetails( } /** - * provider가 세부 사용량을 보고하지 않은 상태를 생성합니다. + * Creates a state in which the provider reported no usage details. * - * @return 모든 세부량이 미보고 상태인 객체 + * @return an object with all details marked as unreported */ public static TokenUsageDetails unreported() { return new TokenUsageDetails(null, null, null); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenizationBasis.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenizationBasis.java index 8418508..f7d6a0f 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenizationBasis.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/TokenizationBasis.java @@ -1,16 +1,17 @@ package io.tokenpilot.core.domain; /** - * token 계산 결과와 모델 encoding의 호환성을 판단할 때 사용하는 계산 기준입니다. + * Basis used to determine compatibility between a token count result and a + * model encoding. * - * @param id tokenization 기준을 식별하는 안정적인 값 + * @param id stable value identifying the tokenization basis */ public record TokenizationBasis(String id) { /** - * tokenization 기준 식별자가 null 또는 blank가 아닌지 검증합니다. + * Validates that the tokenization basis identifier is not null or blank. * - * @throws IllegalArgumentException id가 null이거나 blank인 경우 + * @throws IllegalArgumentException when id is null or blank */ public TokenizationBasis { if (id == null || id.isBlank()) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/UsageSource.java b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/UsageSource.java index b958e7b..810f921 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/domain/UsageSource.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/domain/UsageSource.java @@ -1,17 +1,17 @@ package io.tokenpilot.core.domain; /** - * 토큰 사용량 값의 출처와 생성 방식을 나타냅니다. + * Describes the source and derivation method of token usage values. */ public enum UsageSource { - /** provider가 포괄 총량을 직접 보고한 사용량 */ + /** Usage directly reported by the provider as inclusive totals. */ PROVIDER_REPORTED, - /** provider가 보고한 여러 필드를 어댑터가 정규화해 만든 사용량 */ + /** Usage normalized by an adapter from multiple provider-reported fields. */ PROVIDER_DERIVED, - /** 로컬 tokenizer가 계산한 사용량 */ + /** Usage counted by a local tokenizer. */ LOCAL_TOKENIZER, - /** 휴리스틱으로 근사 추정한 사용량 */ + /** Usage approximately estimated by a heuristic. */ HEURISTIC_ESTIMATE, - /** 사용량 정보를 얻을 수 없음 */ + /** Usage information was unavailable. */ UNAVAILABLE } diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java index 1f8e1fc..2c682ad 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultCostCalculator.java @@ -11,10 +11,10 @@ import java.math.BigDecimal; /** - * 기본 비용 계산기 구현체. - * 포괄 총량에서 cache/reasoning 세부량을 분리한 배타적 구간별로 - * {@link TokenType} 단가를 적용하여 중복 없이 계산합니다. - * 1K 토큰당 가격을 정확한 decimal shift로 계산하며 내부에서 반올림하지 않습니다. + * Default cost calculator implementation. + * Separates cache/reasoning details from inclusive totals into disjoint segments + * and applies the {@link TokenType} rates without double counting. + * Calculates per-1K-token prices with an exact decimal shift and does not round internally. */ class DefaultCostCalculator implements CostCalculator { @Override diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java index 3cba97f..0dd27df 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultLedgerManager.java @@ -10,8 +10,9 @@ import java.util.concurrent.CopyOnWriteArrayList; /** - * 기본 LedgerManager 구현체. - * 가격 저장소와 계산기를 조율하여 비용을 기록하고, 등록된 리스너들에게 이벤트를 전파합니다. + * Default {@link LedgerManager} implementation. + * Coordinates the pricing registry and calculator to record costs and publish + * events to registered listeners. */ class DefaultLedgerManager implements LedgerManager { private static final Currency UNPRICED_COST_CURRENCY = Currency.getInstance("USD"); @@ -33,7 +34,7 @@ public DefaultLedgerManager(PricingRegistry pricingRegistry, CostCalculator cost } /** - * 새로운 리스너를 런타임에 등록합니다. + * Registers a new listener at runtime. */ public void addListener(LedgerListener listener) { if (listener != null) { @@ -77,7 +78,7 @@ private Cost recordResolvedPlan(PricingPlan plan, TokenUsage usage, Map tags) { - // 이벤트 발행 (리스너들에게 전파) + // Publish the event to listeners. if (!listeners.isEmpty()) { CostRecordedEvent event = new CostRecordedEvent(modelId, usage, cost, tags); for (LedgerListener listener : listeners) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultPreflightCostEstimator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultPreflightCostEstimator.java index 67cd8c1..326d957 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultPreflightCostEstimator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultPreflightCostEstimator.java @@ -15,12 +15,14 @@ import java.util.Objects; /** - * 불변 pricing snapshot의 배타적인 입력/출력 가격 경로에서 각각 최대 단가를 선택해 - * 같은 token을 중복 합산하지 않고 보수적인 비용 상한을 계산하는 기본 구현입니다. + * Default implementation that selects the maximum rate from the disjoint input + * and output pricing paths in an immutable pricing snapshot and calculates a + * conservative cost bound without double counting tokens. * - *

이 구현은 비용만 계산하며 context admission이나 provider 호출을 허가하지 않습니다. - * 입력 문맥이 보관한 동일한 snapshot만 사용하며, 계산 중 registry를 다시 조회하지 - * 않습니다. 호출 계층은 그 snapshot을 예약·정산에도 그대로 전달해야 합니다.

+ *

This implementation calculates cost only; it does not authorize context + * admission or provider invocation. It uses only the snapshot retained by the + * input context and does not query a registry again during calculation. The + * calling layer must pass the same snapshot to reservation and settlement.

*/ class DefaultPreflightCostEstimator implements PreflightCostEstimator { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultTokenBudget.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultTokenBudget.java index da34bed..c70b96a 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultTokenBudget.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/DefaultTokenBudget.java @@ -17,7 +17,7 @@ import java.util.OptionalLong; /** - * versioned model registry를 사용해 overflow-safe context admission을 수행합니다. + * Performs overflow-safe context admission using a versioned model registry. */ final class DefaultTokenBudget implements TokenBudget { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/HeuristicTokenEstimator.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/HeuristicTokenEstimator.java index 9f48af7..ce55e05 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/HeuristicTokenEstimator.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/HeuristicTokenEstimator.java @@ -15,11 +15,11 @@ import java.util.Objects; /** - * UTF-8 byte 길이를 기반으로 문자열의 text-only token 수를 추정합니다. + * Estimates text-only token counts from UTF-8 byte length. * - *

{@code BYTE_LEVEL_BPE_UTF8}는 byte-level tokenizer의 안전성 가정을 - * 나타내는 compatibility basis일 뿐, 이 구현이 exact BPE merge를 수행한다는 - * 뜻은 아닙니다. 결과는 전체 요청의 context admission 근거로 사용할 수 없습니다.

+ *

{@code BYTE_LEVEL_BPE_UTF8} is only a compatibility basis representing a + * byte-level tokenizer safety assumption; this implementation does not perform + * exact BPE merges. The result cannot prove context admission for a complete request.

*/ final class HeuristicTokenEstimator implements TokenEstimator { @@ -38,12 +38,12 @@ final class HeuristicTokenEstimator implements TokenEstimator { new TokenizationBasis(TOKENIZATION_BASIS_ID); /** - * 원문을 정규화하지 않고 UTF-8 byte 길이로 계산합니다. + * Counts UTF-8 byte length without normalizing the original text. * - * @param text 계산할 원문 - * @return text-only 휴리스틱 token 계산 결과 - * @throws NullPointerException text가 null인 경우 - * @throws IllegalArgumentException text에 malformed UTF-16이 포함된 경우 + * @param text original text to count + * @return text-only heuristic token count result + * @throws NullPointerException when text is null + * @throws IllegalArgumentException when text contains malformed UTF-16 */ @Override public TokenCountResult estimate(String text) { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryModelRegistry.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryModelRegistry.java index 558573f..da3970b 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryModelRegistry.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryModelRegistry.java @@ -10,7 +10,7 @@ import java.util.Optional; /** - * canonical id와 exact alias를 하나의 lookup index로 관리하는 in-memory registry입니다. + * In-memory registry that manages canonical IDs and exact aliases in one lookup index. */ final class InMemoryModelRegistry implements ModelRegistry { diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java index 926c7c8..fe0293f 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/InMemoryPricingRegistry.java @@ -18,7 +18,7 @@ /** - * 메모리 기반 가격 정책 저장소 구현체. + * In-memory pricing policy repository implementation. */ class InMemoryPricingRegistry implements PricingRegistry { private final Map plans = new ConcurrentHashMap<>(); diff --git a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/LedgerComponents.java b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/LedgerComponents.java index 6267212..34753ff 100644 --- a/token-pilot-core/src/main/java/io/tokenpilot/core/internal/LedgerComponents.java +++ b/token-pilot-core/src/main/java/io/tokenpilot/core/internal/LedgerComponents.java @@ -17,7 +17,7 @@ import java.util.List; /** - * Token Pilot 코어 컴포넌트 생성을 위한 팩토리 클래스입니다. + * Factory for creating Token Pilot core components. */ public final class LedgerComponents { @@ -33,29 +33,29 @@ public static PricingEvaluator defaultPricingEvaluator() { } /** - * 기본 preflight 비용 상한 계산기를 생성합니다. + * Creates the default preflight cost-bound estimator. * - *

계산기는 REQUEST 범위의 token 결과와 하나의 불변 pricing snapshot을 - * 보관한 문맥을 요구합니다. 이 factory는 context admission이나 atomic reservation을 - * 대신 수행하지 않습니다.

+ *

The estimator requires a context retaining a REQUEST-scope token result + * and one immutable pricing snapshot. This factory does not perform context + * admission or atomic reservation.

* - * @return 기본 preflight 비용 상한 계산기 + * @return default preflight cost-bound estimator */ public static PreflightCostEstimator defaultPreflightCostEstimator() { return new DefaultPreflightCostEstimator(); } /** - * UTF-8 byte 길이 기반의 text-only 휴리스틱 token estimator를 생성합니다. + * Creates a text-only heuristic token estimator based on UTF-8 byte length. * - * @return UTF-8 byte 휴리스틱 estimator + * @return UTF-8 byte heuristic estimator */ public static TokenEstimator utf8ByteHeuristicTokenEstimator() { return new HeuristicTokenEstimator(); } /** - * model registry를 사용하는 context admission evaluator를 생성합니다. + * Creates a context admission evaluator backed by a model registry. * * @param modelRegistry versioned model catalog * @return token context budget evaluator @@ -65,7 +65,8 @@ public static TokenBudget tokenBudget(ModelRegistry modelRegistry) { } /** - * model registry와 best-effort decision listener를 사용하는 context admission evaluator를 생성합니다. + * Creates a context admission evaluator backed by a model registry and + * best-effort decision listeners. */ public static TokenBudget tokenBudget( ModelRegistry modelRegistry, @@ -75,9 +76,9 @@ public static TokenBudget tokenBudget( } /** - * 등록할 model definition으로 in-memory model registry를 생성합니다. + * Creates an in-memory model registry from model definitions to register. * - * @param definitions immutable model catalog 정의 + * @param definitions immutable model catalog definitions * @return in-memory model registry */ public static ModelRegistry inMemoryModelRegistry(Collection definitions) { @@ -85,9 +86,10 @@ public static ModelRegistry inMemoryModelRegistry(Collection de } /** - * 공개 문서의 확인 시점과 encoding metadata를 고정한 최소 기본 catalog를 생성합니다. + * Creates the minimal default catalog with public-document verification times + * and encoding metadata fixed. * - * @return gpt-4o와 gpt-4o-mini versioned catalog + * @return versioned catalog for gpt-4o and gpt-4o-mini */ public static ModelRegistry defaultModelRegistry() { return inMemoryModelRegistry(DefaultModelCatalog.definitions()); diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/MetricsOptions.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/MetricsOptions.java index 343e470..fc0fd32 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/MetricsOptions.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/MetricsOptions.java @@ -22,11 +22,11 @@ public record MetricsOptions( } /** - * 기존 직접 생성 코드의 source/binary 호환성을 유지하는 생성자입니다. + * Constructor preserving source and binary compatibility with existing direct construction code. * - *

이 생성자를 직접 사용하는 것은 legacy {@code ai.token.*} meter에 대한 명시적 - * opt-in으로 취급됩니다. Starter 자동 설정 기본값은 {@link #defaults()}를 사용하여 - * legacy meter를 비활성화합니다.

+ *

Using this constructor directly is treated as an explicit opt-in to + * legacy {@code ai.token.*} meters. Starter auto-configuration uses + * {@link #defaults()} by default to disable legacy meters.

*/ public MetricsOptions(Set allowedTagKeys) { this( diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMetricsPublisher.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMetricsPublisher.java index 8b46648..3480124 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMetricsPublisher.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMetricsPublisher.java @@ -20,7 +20,7 @@ import java.math.BigInteger; import java.util.Objects; -/** Atomic budget/accounting 결과를 Token Pilot 고유 Micrometer meter로 투영합니다. */ +/** Projects atomic budget/accounting results into Token Pilot-owned Micrometer meters. */ public final class BudgetMetricsPublisher implements ReservationAccountingListener, ReservationAccountingListenerErrorHandler { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMicrometerComponents.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMicrometerComponents.java index 754185a..d2a288a 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMicrometerComponents.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/BudgetMicrometerComponents.java @@ -2,7 +2,7 @@ import io.micrometer.core.instrument.MeterRegistry; -/** Atomic budget owner의 선택 Micrometer publisher 팩토리입니다. */ +/** Factory for the optional Micrometer publisher owned by the atomic budget component. */ public final class BudgetMicrometerComponents { private BudgetMicrometerComponents() { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/CoreMetricsPublisher.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/CoreMetricsPublisher.java index 7672c76..ee6cdfe 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/CoreMetricsPublisher.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/CoreMetricsPublisher.java @@ -9,7 +9,7 @@ import java.util.Objects; -/** Token Pilot core의 preflight와 pricing-miss 이벤트를 Micrometer meter로 투영합니다. */ +/** Projects Token Pilot core preflight and pricing-miss events into Micrometer meters. */ public final class CoreMetricsPublisher implements PreflightDecisionListener, PricingMissingListener { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/LedgerMicrometerComponents.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/LedgerMicrometerComponents.java index 404d760..0c664bf 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/LedgerMicrometerComponents.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/LedgerMicrometerComponents.java @@ -7,7 +7,7 @@ import java.util.Set; /** - * Micrometer 메트릭 퍼블리셔 생성을 위한 팩토리 클래스입니다. + * Factory for creating Micrometer metric publishers. */ public final class LedgerMicrometerComponents { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisher.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisher.java index 8c533e6..dd729ee 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisher.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisher.java @@ -16,9 +16,9 @@ import java.util.Set; /** - * Micrometer 기반의 비용 메트릭 리스너. - * {@link LedgerListener}를 구현하여 비용 기록 이벤트를 가로채고, - * 이를 Prometheus 등 모니터링 시스템으로 전송합니다. + * Micrometer-based cost metric listener. + * Implements {@link LedgerListener} to intercept cost-recording events and + * publish them to monitoring systems such as Prometheus. */ public class MicroCostMetricsPublisher implements LedgerListener { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMetricsPublisher.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMetricsPublisher.java index 04e35b3..77a35e7 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMetricsPublisher.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMetricsPublisher.java @@ -7,7 +7,7 @@ import java.util.Objects; -/** Budget notification lifecycle 이벤트를 Micrometer meter로 투영합니다. */ +/** Projects budget notification lifecycle events into Micrometer meters. */ public final class NotificationMetricsPublisher implements BudgetNotificationLifecycleListener { diff --git a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMicrometerComponents.java b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMicrometerComponents.java index 023e2ee..9d1fe5d 100644 --- a/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMicrometerComponents.java +++ b/token-pilot-micrometer/src/main/java/io/tokenpilot/micrometer/internal/NotificationMicrometerComponents.java @@ -2,7 +2,7 @@ import io.micrometer.core.instrument.MeterRegistry; -/** Budget notification owner의 선택 Micrometer publisher 팩토리입니다. */ +/** Factory for the optional Micrometer publisher owned by budget notifications. */ public final class NotificationMicrometerComponents { private NotificationMicrometerComponents() { diff --git a/token-pilot-micrometer/src/test/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisherTest.java b/token-pilot-micrometer/src/test/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisherTest.java index 314bd9a..0c8ba94 100644 --- a/token-pilot-micrometer/src/test/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisherTest.java +++ b/token-pilot-micrometer/src/test/java/io/tokenpilot/micrometer/internal/MicroCostMetricsPublisherTest.java @@ -39,7 +39,7 @@ void shouldPublishMetricsWhenEventRecorded() { // When publisher.onRecord(event); - // Then: 토큰 사용량 카운터 확인 + // Then: verify the token usage counter assertThat(meterRegistry.find("ai.token.usage.total") .tag("model", "gpt-4o") .tag("token_type", "prompt") @@ -50,13 +50,13 @@ void shouldPublishMetricsWhenEventRecorded() { .tag("token_type", "completion") .counter().count()).isEqualTo(200.0); - // Then: 토큰 사용량 분포(Summary) 확인 + // Then: verify the token usage distribution summary assertThat(meterRegistry.find("ai.token.usage.distribution") .tag("model", "gpt-4o") .tag("token_type", "prompt") .summary().max()).isEqualTo(100.0); - // Then: 비용 카운터 확인 + // Then: verify the cost counter var costCounter = meterRegistry.find("ai.token.cost.total") .tag("model", "gpt-4o") .tag("currency", "USD") diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/AtomicNotificationStateStore.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/AtomicNotificationStateStore.java index 35b791e..2fc8e81 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/AtomicNotificationStateStore.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/AtomicNotificationStateStore.java @@ -6,7 +6,7 @@ import io.tokenpilot.budget.ReservationAccountingEvent; import io.tokenpilot.budget.ReservationReconciliationRequiredEvent; -/** atomic budget/accounting 결과의 threshold를 원자적으로 claim하는 저장소 계약입니다. */ +/** Store contract for atomically claiming thresholds from atomic budget/accounting results. */ public interface AtomicNotificationStateStore extends NotificationStateStore { NotificationClaim recordAppliedTransition( diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationError.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationError.java index ddc6dd2..5be0453 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationError.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationError.java @@ -3,10 +3,11 @@ import java.util.Objects; /** - * 알림 실패 hook에 전달하는 제한된 오류 정보입니다. + * Bounded error information passed to the notification failure hook. * - *

원본 예외 메시지와 notification event를 보존하지 않아 prompt, provider response, - * API key가 오류 경로로 전달되지 않습니다. 각 문자열은 최대 128자로 제한됩니다.

+ *

The original exception message and notification event are not retained, so + * prompts, provider responses, and API keys do not enter the error path. Each + * string is limited to 128 characters.

*/ public record BudgetNotificationError( String stage, diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationErrorHook.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationErrorHook.java index 6a17978..3da7df9 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationErrorHook.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationErrorHook.java @@ -1,6 +1,6 @@ package io.tokenpilot.notification; -/** bounded/sanitized notification 오류를 관찰하는 best-effort hook입니다. */ +/** Best-effort hook for observing bounded and sanitized notification errors. */ @FunctionalInterface public interface BudgetNotificationErrorHook { diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationEvent.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationEvent.java index 2625449..1ed18c5 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationEvent.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationEvent.java @@ -9,21 +9,22 @@ import java.util.Objects; /** - * 예산 임계치 도달 시 발생하는 알림 이벤트입니다. + * Notification event emitted when a budget threshold is reached. * - * @param key 예산 bucket 식별자 - * @param threshold 도달한 임계치 - * @param state 예산 상태 - * @param reason 상태 설명 - * @param usage 알림을 만든 accounting 또는 admission 결과의 사용량 - * @param limit 예산 한도 - * @param source 알림을 만든 원자적 결과 종류 - * @param tags legacy decision 알림에서만 보존하는 호환 tag + * @param key budget bucket identifier + * @param threshold reached threshold + * @param state budget state + * @param reason state description + * @param usage usage from the accounting or admission result that produced the notification + * @param limit budget limit + * @param source atomic result type that produced the notification + * @param tags compatibility tags retained only for legacy decision notifications * - *

원자적 accounting 이벤트는 prompt, raw provider response, API key, 임의 tag map을 - * 포함하지 않습니다. legacy decision 생성자만 기존 handler 호환을 위해 불변 tag 복사본을 - * 보존합니다. {@link #projectedUsage()}와 {@link #currentUsage()} 호환 accessor는 0.1.x 동안 - * 유지하며 0.2.0에서 제거할 예정입니다.

+ *

Atomic accounting events do not contain prompts, raw provider responses, + * API keys, or arbitrary tag maps. Only the legacy decision constructor retains + * an immutable tag copy for handler compatibility. The compatibility accessors + * {@link #projectedUsage()} and {@link #currentUsage()} remain through 0.1.x + * and are scheduled for removal in 0.2.0.

*/ public record BudgetNotificationEvent( BudgetKey key, @@ -70,7 +71,8 @@ public BudgetNotificationEvent( } /** - * @deprecated legacy decision 기반 이벤트 생성 호환용입니다. tags는 불변 복사본으로 보존됩니다. + * @deprecated Compatibility constructor for legacy decision-based event creation. + * Tags are retained as an immutable copy. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetNotificationEvent( @@ -95,8 +97,8 @@ public BudgetNotificationEvent( } /** - * @return {@link #usage()}와 동일한 accounting/admission 사용량 - * @deprecated source별 의미가 명확한 {@link #usage()}를 사용하세요. + * @return accounting/admission usage identical to {@link #usage()} + * @deprecated Use {@link #usage()}, whose meaning is clear for each source. */ @Deprecated(since = "0.1.0", forRemoval = true) public Cost projectedUsage() { @@ -104,8 +106,8 @@ public Cost projectedUsage() { } /** - * @return {@link #usage()}와 동일한 accounting/admission 사용량 - * @deprecated source별 의미가 명확한 {@link #usage()}를 사용하세요. + * @return accounting/admission usage identical to {@link #usage()} + * @deprecated Use {@link #usage()}, whose meaning is clear for each source. */ @Deprecated(since = "0.1.0", forRemoval = true) public Cost currentUsage() { diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationHandler.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationHandler.java index 5bce627..c66264a 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationHandler.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationHandler.java @@ -1,9 +1,9 @@ package io.tokenpilot.notification; /** - * 알림 이벤트를 처리하는 인터페이스 + * Interface for handling notification events. */ public interface BudgetNotificationHandler { void handle(BudgetNotificationEvent event); -} \ No newline at end of file +} diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleEvent.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleEvent.java index 3ebabea..5436efb 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleEvent.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleEvent.java @@ -5,10 +5,11 @@ import java.util.Objects; /** - * notification handler 처리 또는 dedup 결과를 전달하는 framework-independent 이벤트입니다. + * Framework-independent event carrying the result of notification handler + * processing or deduplication. * - * @param outcome success, failure 또는 process-local dedup 결과 - * @param threshold 처리 대상인 bounded budget threshold + * @param outcome success, failure, or process-local deduplication result + * @param threshold bounded budget threshold being processed */ public record BudgetNotificationLifecycleEvent( BudgetNotificationOutcome outcome, diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleListener.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleListener.java index d1d6e0b..4f7f573 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleListener.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationLifecycleListener.java @@ -1,6 +1,6 @@ package io.tokenpilot.notification; -/** Budget notification lifecycle을 best-effort로 관찰하는 framework-independent 계약입니다. */ +/** Framework-independent contract for best-effort observation of the budget notification lifecycle. */ @FunctionalInterface public interface BudgetNotificationLifecycleListener { diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationOutcome.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationOutcome.java index 0056630..64731a2 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationOutcome.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationOutcome.java @@ -1,6 +1,6 @@ package io.tokenpilot.notification; -/** Budget notification 처리의 bounded 운영 결과입니다. */ +/** Bounded operational result of budget notification processing. */ public enum BudgetNotificationOutcome { SUCCESS, FAILURE, diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java index 562df54..8b8bbb1 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationService.java @@ -21,14 +21,15 @@ import java.util.function.Supplier; /** - * 원자적 budget/accounting 결과를 threshold 알림으로 변환하는 best-effort listener입니다. + * Best-effort listener that converts atomic budget/accounting results into threshold notifications. * - *

원자적 callback의 threshold claim은 handler 호출 전에 완료됩니다. handler 실패는 다음 - * handler 전달을 막지 않고 이미 적용된 회계 또는 admission 결과를 변경하지 않으며, 실패한 - * 전달은 durable outbox가 없는 MVP에서 재시도되지 않습니다. Legacy decision 경로는 호환성을 - * 위해 전달 성공 후에만 상태를 갱신하므로 실패한 호출을 다음 호출에서 다시 시도합니다. - * 두 경로의 claim은 같은 store monitor에서 직렬화되어 혼용 중에도 같은 threshold를 중복 - * 전달하지 않습니다.

+ *

Threshold claims for atomic callbacks complete before handlers are called. + * A handler failure does not block the next handler or change an already applied + * accounting/admission result, and failed delivery is not retried in the MVP + * because there is no durable outbox. For compatibility, the legacy decision + * path updates state only after successful delivery, so a failed call is retried + * on the next call. Claims from both paths are serialized on the same store + * monitor and do not deliver the same threshold twice when mixed.

*/ public class BudgetNotificationService implements ReservationAccountingListener { @@ -40,8 +41,9 @@ public class BudgetNotificationService implements ReservationAccountingListener private final List lifecycleListeners; /** - * @deprecated legacy {@link BudgetDecision} 알림 호환용 생성자입니다. 신규 연결은 복수 - * handler와 error hook을 받는 생성자를 사용하세요. + * @deprecated Compatibility constructor for legacy {@link BudgetDecision} + * notifications. New wiring should use the constructor accepting + * multiple handlers and an error hook. */ @Deprecated(since = "0.1.0", forRemoval = false) public BudgetNotificationService( @@ -51,7 +53,7 @@ public BudgetNotificationService( this(handler, store, List.of()); } - /** legacy decision 알림에 lifecycle 관측을 추가하는 호환 생성자입니다. */ + /** Compatibility constructor that adds lifecycle observation to legacy decision notifications. */ public BudgetNotificationService( BudgetNotificationHandler handler, NotificationStateStore store, @@ -134,9 +136,10 @@ private BudgetNotificationService( } /** - * 기존 #37 callback은 주입된 resolver로 현재 bucket snapshot을 조회합니다. 현재 budget store는 - * 정확한 전이 시점 snapshot을 포함하는 - * {@link #onAccountingApplied(ReservationAccountingEvent, BudgetSnapshot)}를 호출합니다. + * The former #37 callback reads the current bucket snapshot through the + * injected resolver. The current budget store calls + * {@link #onAccountingApplied(ReservationAccountingEvent, BudgetSnapshot)} + * with the snapshot from the exact transition point. */ @Override public void onCommitted(ReservationAccountingEvent event) { @@ -244,9 +247,10 @@ public void onReservationBlocked( } /** - * @deprecated legacy evaluator 결과는 admission/accounting 알림 근거가 아닙니다. 신규 코드는 - * {@link ReservationAccountingListener} 연결을 사용하세요. tags는 legacy 이벤트에서만 - * 불변 복사본으로 보존됩니다. + * @deprecated Legacy evaluator results are not grounds for admission/accounting + * notifications. New code should wire a + * {@link ReservationAccountingListener}. Tags are retained as an + * immutable copy only for legacy events. */ @Deprecated(since = "0.1.0", forRemoval = false) public void notifyIfNeeded( @@ -351,7 +355,7 @@ private void report(BudgetNotificationError error) { try { errorHook.onError(error); } catch (RuntimeException ignored) { - // Error hook도 best-effort이며 accounting/provider 결과에 영향을 주지 않습니다. + // The error hook is also best-effort and does not affect accounting or provider results. } } diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationSource.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationSource.java index 16f70a0..c44a210 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationSource.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/BudgetNotificationSource.java @@ -1,6 +1,6 @@ package io.tokenpilot.notification; -/** 알림을 만든 원자적 budget/accounting 결과의 종류입니다. */ +/** Type of atomic budget/accounting result that produced a notification. */ public enum BudgetNotificationSource { ACCOUNTING_COMMIT, RECONCILIATION_REQUIRED, diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java index da7873a..f731fe1 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/InMemoryNotificationStateStore.java @@ -22,12 +22,14 @@ import java.util.concurrent.atomic.AtomicReference; /** - * 메모리 기반 accounting threshold dedup 저장소입니다. + * In-memory accounting threshold deduplication store. * - *

dedup key는 {@code BudgetKey(policy/target/window) + BudgetThreshold}입니다. - * 처리한 accounting transition은 같은 bucket의 {@link ReservationId}로 중복 claim을 막습니다. - * 상태는 이 store 인스턴스의 생명주기 동안 보존되며 재시작 후 replay나 TTL 정리는 제공하지 - * 않습니다. 따라서 MVP 전달 보장은 process-local best-effort이며 durable exactly-once가 아닙니다.

+ *

The deduplication key is {@code BudgetKey(policy/target/window) + BudgetThreshold}. + * Processed accounting transitions are protected from duplicate claims by + * {@link ReservationId} within the same bucket. State is retained for the + * lifetime of this store instance; restart replay and TTL cleanup are not + * provided. Therefore MVP delivery is process-local best-effort, not durable + * exactly-once.

*/ @SuppressWarnings("deprecation") public class InMemoryNotificationStateStore diff --git a/token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java b/token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java index da73370..255fb51 100644 --- a/token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java +++ b/token-pilot-notification/src/main/java/io/tokenpilot/notification/NotificationStateStore.java @@ -8,23 +8,24 @@ import java.util.Objects; /** - * legacy decision 기반 알림 dedup 저장소입니다. - * Atomic lifecycle 연결은 {@link AtomicNotificationStateStore}를 구현해야 합니다. + * Notification deduplication store for legacy decision-based notifications. + * Atomic lifecycle integration requires implementing + * {@link AtomicNotificationStateStore}. */ public interface NotificationStateStore { - /** @deprecated legacy decision 기반 조회 API입니다. */ + /** @deprecated Legacy decision-based read API. */ @Deprecated(since = "0.1.0", forRemoval = false) BudgetThreshold getLastNotifiedThreshold(BudgetKey key); - /** @deprecated legacy decision 기반 갱신 API입니다. */ + /** @deprecated Legacy decision-based update API. */ @Deprecated(since = "0.1.0", forRemoval = false) void updateLastNotifiedThreshold( BudgetKey key, BudgetThreshold threshold ); - /** 원자적으로 claim된 threshold와 해당 판단의 사용량입니다. */ + /** Atomically claimed thresholds and the usage from their decision. */ record NotificationClaim( Cost usage, List thresholds, diff --git a/token-pilot-sample-app/DEMO_RUNBOOK.md b/token-pilot-sample-app/DEMO_RUNBOOK.md index 2376c1f..bd76e09 100644 --- a/token-pilot-sample-app/DEMO_RUNBOOK.md +++ b/token-pilot-sample-app/DEMO_RUNBOOK.md @@ -1,4 +1,6 @@ -# Token Pilot demo runbook +# Token Pilot demo runbook (Korean) + +영문 버전은 [`DEMO_RUNBOOK_EN.md`](DEMO_RUNBOOK_EN.md)에서 확인할 수 있습니다. 이 데모는 `demo` Spring profile에서만 활성화되는 네트워크 없는 가짜 `ChatModel`을 사용합니다. 별도 자격 증명이나 외부 호출 없이도 context admission, @@ -89,7 +91,55 @@ curl -s http://localhost:8080/actuator/prometheus \ docker compose -f token-pilot-sample-app/docker-compose.yml down ``` -## 4. 상태 초기화 +## 4. OpenAI provider smoke 시나리오 + +`demo` 앱을 먼저 종료한 뒤(`Ctrl+C`) 실제 OpenAI 호출을 수행합니다. 이 경로는 +외부 네트워크와 API key를 사용하며, 호출 비용은 OpenAI 계정에 청구됩니다. API key를 +소스 파일이나 커밋에 넣지 말고 환경 변수로만 전달합니다. + +기본 smoke 모델은 Token Pilot catalog에 등록된 +`gpt-4o-mini-2024-07-18`입니다. 다른 모델을 사용하려면 Token Pilot catalog와 +가격 설정도 함께 맞춰야 합니다. + +```bash +export OPENAI_API_KEY='발급받은 OpenAI API key' +export OPENAI_MODEL='gpt-4o-mini-2024-07-18' + +./gradlew --no-daemon :token-pilot-sample-app:bootRun \ + --args='--spring.profiles.active=openai-smoke' +``` + +다른 터미널에서 한 번 호출합니다. + +```bash +curl -sS --get http://localhost:8080/test/token-pilot/openai-smoke \ + --data-urlencode 'prompt=Reply with one short sentence confirming the provider is reachable.' \ + | jq +``` + +정상 결과는 `status=PASS`, `usageSource`가 `UNAVAILABLE`이 아닌 값, 그리고 +`accountingState=COMMITTED`입니다. 응답에는 provider 응답 토큰 수와 실제 정산 비용도 +함께 포함됩니다. + +```bash +curl -sS http://localhost:8080/actuator/prometheus \ + | rg '^tokenpilot_(preflight|budget|cost|reconciliation)' +``` + +자동화된 smoke test를 직접 실행할 때는 반드시 두 조건을 명시합니다. 일반적인 +`./gradlew test`에서는 실제 호출이 발생하지 않습니다. + +```bash +RUN_OPENAI_SMOKE=true \ + ./gradlew --no-daemon :token-pilot-sample-app:test \ + --tests io.tokenpilot.sample.OpenAiSmokeE2ETest +``` + +demo와 smoke를 연속으로 확인하려면 `demo` 프로필에서 `/demo/run`을 실행하고 앱을 +종료한 다음, 위의 `openai-smoke` 프로필을 다시 시작해 smoke endpoint를 호출합니다. +두 프로필은 같은 `8080` 포트를 사용하므로 동시에 실행하지 않습니다. + +## 5. 상태 초기화 앱을 종료한 뒤 다시 `bootRun`하면 in-memory budget과 Micrometer counter가 초기화됩니다. diff --git a/token-pilot-sample-app/DEMO_RUNBOOK_EN.md b/token-pilot-sample-app/DEMO_RUNBOOK_EN.md new file mode 100644 index 0000000..e8991af --- /dev/null +++ b/token-pilot-sample-app/DEMO_RUNBOOK_EN.md @@ -0,0 +1,150 @@ +# Token Pilot demo runbook (English) + +This demo uses a network-free fake `ChatModel` that is enabled only in the +`demo` Spring profile. It repeatedly exercises context admission, atomic +reservation, idempotency, release, and actual reconciliation without separate +credentials or external calls. + +## 1. Start the application + +Run the following from the repository root: + +```bash +./gradlew --no-daemon :token-pilot-sample-app:bootRun \ + --args='--spring.profiles.active=demo' +``` + +The application listens on `http://localhost:8080`. No API key is required. + +## 2. Run the scenarios + +Show the demo status and scenario index first: + +```bash +curl -s http://localhost:8080/test/token-pilot/demo | jq +``` + +Run all eight scenarios and print the key verification fields: + +```bash +curl -s http://localhost:8080/test/token-pilot/demo/run | jq '{ + runId, + scenarios: [.scenarios[] | { + scenario, + status, + providerInvocations: .evidence.providerInvocations, + admittedRequests: .evidence.admittedRequests, + blockedRequests: .evidence.blockedRequests, + accountingState: .evidence.accountingState, + budget: .evidence.budget + }], + metrics +}' +``` + +Expected results: + +| Scenario | Expected result | +| --- | --- | +| `context-fit` | `PASS`, admission `FITS`, provider invocations `0` | +| `context-block` | `PASS`, admission `EXCEEDS`, provider invocations `0` | +| `budget-concurrency` | `PASS`, one admitted request, seven blocked requests, one provider invocation | +| `idempotency` | `PASS`, the repeated key is reused and the provider is called once | +| `release` | `PASS`, the reservation is released before dispatch | +| `reconciliation-success` | `PASS`, accounting state `COMMITTED` | +| `reconciliation-failure` | `PASS`, accounting state `RECONCILIATION_REQUIRED` | +| `reconciliation-unknown` | `PASS`, accounting state `RECONCILIATION_REQUIRED` | + +Run an individual scenario when recording a focused result: + +```bash +curl -s http://localhost:8080/test/token-pilot/demo/context-block | jq +curl -s http://localhost:8080/test/token-pilot/demo/budget-concurrency | jq +curl -s http://localhost:8080/test/token-pilot/demo/idempotency | jq +curl -s http://localhost:8080/test/token-pilot/demo/reconciliation-failure | jq +``` + +## 3. Start Prometheus and Grafana + +Keep the application running on the host and start only the monitoring +containers: + +```bash +docker compose -f token-pilot-sample-app/docker-compose.yml up --build -d +``` + +Open these addresses: + +- Prometheus targets: `http://localhost:9090/targets` — `token-pilot-backend` should be `UP` +- Prometheus query UI: `http://localhost:9090/graph` +- Grafana: `http://localhost:3000` — select the `Token Pilot Demo` dashboard + +Prometheus scrapes `host.docker.internal:8080/actuator/prometheus`. Run the demo +endpoint, wait about ten seconds, and then inspect the `tokenpilot_*` values in +Grafana or directly from the endpoint: + +```bash +curl -s http://localhost:8080/actuator/prometheus \ + | rg '^tokenpilot_(preflight|budget|cost|reconciliation)' +``` + +Stop the monitoring containers when finished: + +```bash +docker compose -f token-pilot-sample-app/docker-compose.yml down +``` + +## 4. Run the OpenAI provider smoke scenario + +Stop the `demo` application first with `Ctrl+C`, then start the real OpenAI +provider path. This path uses external network access and an API key; provider +charges may apply. Never put the key in a source file or commit it. + +The default smoke model is the Token Pilot catalog entry +`gpt-4o-mini-2024-07-18`. If you choose another model, update the Token Pilot +catalog and pricing configuration to match it. + +```bash +export OPENAI_API_KEY='your OpenAI API key' +export OPENAI_MODEL='gpt-4o-mini-2024-07-18' + +./gradlew --no-daemon :token-pilot-sample-app:bootRun \ + --args='--spring.profiles.active=openai-smoke' +``` + +In a second terminal, send one short request: + +```bash +curl -sS --get http://localhost:8080/test/token-pilot/openai-smoke \ + --data-urlencode 'prompt=Reply with one short sentence confirming the provider is reachable.' \ + | jq +``` + +A successful response reports `status=PASS`, a `usageSource` other than +`UNAVAILABLE`, and `accountingState=COMMITTED`. It also includes normalized +provider usage and the resulting accounting cost. + +Inspect the resulting meters: + +```bash +curl -sS http://localhost:8080/actuator/prometheus \ + | rg '^tokenpilot_(preflight|budget|cost|reconciliation)' +``` + +The automated smoke test is opt-in and requires both conditions below. A normal +`./gradlew test` run does not make a provider call. + +```bash +RUN_OPENAI_SMOKE=true \ + ./gradlew --no-daemon :token-pilot-sample-app:test \ + --tests io.tokenpilot.sample.OpenAiSmokeE2ETest +``` + +To run both paths for a recording, execute `/test/token-pilot/demo/run` with the +`demo` profile, stop the application, then restart it with `openai-smoke` and +call the smoke endpoint. Both profiles use port `8080`, so do not run them at +the same time. + +## 5. Reset the demo state + +Restarting `bootRun` resets the in-memory budget store and Micrometer counters. diff --git a/token-pilot-sample-app/Dockerfile b/token-pilot-sample-app/Dockerfile index b6cda0c..a8fb958 100644 --- a/token-pilot-sample-app/Dockerfile +++ b/token-pilot-sample-app/Dockerfile @@ -1,13 +1,13 @@ # token-pilot-sample-app/Dockerfile # ========================================== -# 실행 환경 (Java 25 규격 적용) - 빌드는 GitHub Actions가 담당 +# Runtime environment (Java 25); GitHub Actions performs the build. # ========================================== FROM eclipse-temurin:25-jre WORKDIR /app -# 깃허브 액션이 미리 빌드해둔 jar 파일을 컨테이너 내부로 복사 +# Copy the JAR prebuilt by GitHub Actions into the container. COPY token-pilot-sample-app/build/libs/*SNAPSHOT.jar ./app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/token-pilot-sample-app/build.gradle b/token-pilot-sample-app/build.gradle index 6d316e2..1b9f705 100644 --- a/token-pilot-sample-app/build.gradle +++ b/token-pilot-sample-app/build.gradle @@ -13,11 +13,12 @@ repositories { dependencies { implementation project(':token-pilot-starter') + implementation 'org.springframework.ai:spring-ai-starter-model-openai' - // 웹 서버 구동을 위한 필수품 + // Required for running the web server. implementation 'org.springframework.boot:spring-boot-starter-web' - // 모니터링 (프로메테우스가 데이터를 긁어갈 수 있게 해줌) + // Monitoring support (allows Prometheus to scrape data). implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' @@ -26,12 +27,12 @@ dependencies { testImplementation 'org.springframework.ai:spring-ai-client-chat' } -// 스프링 부트 실행 파일(밀키트) 포장을 강제로 켭니다! +// Force packaging of the Spring Boot executable. bootJar { enabled = true } -// 일반 부품용 jar 포장은 필요 없으니 끕니다. +// Disable the regular library JAR because the executable is the intended artifact. jar { enabled = false } diff --git a/token-pilot-sample-app/docker-compose.yml b/token-pilot-sample-app/docker-compose.yml index b658184..c871802 100644 --- a/token-pilot-sample-app/docker-compose.yml +++ b/token-pilot-sample-app/docker-compose.yml @@ -1,10 +1,10 @@ services: prometheus: - build: ./prometheus # 👈 그라파나처럼 직접 굽기로 통일! + build: ./prometheus # 👈 Use a local build consistently, like Grafana. container_name: prometheus ports: - "9090:9090" - extra_hosts: # 👈 로컬 호스트(스프링 부트)와 통신하기 위한 뚫기 + extra_hosts: # 👈 Allow communication with the local Spring Boot host. - "host.docker.internal:host-gateway" grafana: @@ -19,4 +19,4 @@ services: - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_NAME=Main Org. - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer - - GF_AUTH_DISABLE_LOGIN_FORM=true # 별도 로그인 없이 대시보드 조회 + - GF_AUTH_DISABLE_LOGIN_FORM=true # View the dashboard without a separate login form. diff --git a/token-pilot-sample-app/grafana/provisioning/datasources/datasource.yml b/token-pilot-sample-app/grafana/provisioning/datasources/datasource.yml index 43735de..63600e9 100644 --- a/token-pilot-sample-app/grafana/provisioning/datasources/datasource.yml +++ b/token-pilot-sample-app/grafana/provisioning/datasources/datasource.yml @@ -4,6 +4,6 @@ datasources: - name: Prometheus type: prometheus access: proxy - url: ${PROMETHEUS_URL} # 고정된 주소 대신, 환경 변수로 구멍을 뚫어둡니다! 로컬과 aws 둘 다 사용하기 위함 + url: ${PROMETHEUS_URL} # Use an environment variable instead of a fixed address for local and AWS use. isDefault: true - uid: prometheus-uid \ No newline at end of file + uid: prometheus-uid diff --git a/token-pilot-sample-app/prometheus/Dockerfile b/token-pilot-sample-app/prometheus/Dockerfile index 32512fc..ef9b120 100644 --- a/token-pilot-sample-app/prometheus/Dockerfile +++ b/token-pilot-sample-app/prometheus/Dockerfile @@ -1,4 +1,4 @@ FROM prom/prometheus:v2.45.0 -# 우리가 작성한 커스텀 설정 파일을 프로메테우스 설정 경로로 덮어씌웁니다. -COPY prometheus.yml /etc/prometheus/prometheus.yml \ No newline at end of file +# Replace the Prometheus configuration path with our custom configuration file. +COPY prometheus.yml /etc/prometheus/prometheus.yml diff --git a/token-pilot-sample-app/prometheus/prometheus.yml b/token-pilot-sample-app/prometheus/prometheus.yml index e3cddd0..84cf7fa 100644 --- a/token-pilot-sample-app/prometheus/prometheus.yml +++ b/token-pilot-sample-app/prometheus/prometheus.yml @@ -5,6 +5,6 @@ scrape_configs: - job_name: 'token-pilot-backend' metrics_path: '/actuator/prometheus' static_configs: - # demo profile의 Spring Boot 앱은 호스트에서 실행하고, - # Prometheus는 host.docker.internal을 통해 scrape합니다. + # The Spring Boot app in the demo profile runs on the host, + # and Prometheus scrapes it through host.docker.internal. - targets: ['host.docker.internal:8080'] diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoChatModel.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoChatModel.java index 9561e42..e6b8641 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoChatModel.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoChatModel.java @@ -17,11 +17,12 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * 녹화와 로컬 검증을 위한 네트워크 없는 Spring AI provider입니다. + * Network-free Spring AI provider for recording and local verification. * - *

기본 프로필에는 등록되지 않으며 {@code demo} 프로필에서만 사용됩니다. - * provider usage, 실패, usage 누락, 동시 dispatch 대기를 모두 결정적으로 재현할 수 - * 있어 API key 없이도 adapter와 accounting lifecycle을 확인할 수 있습니다.

+ *

It is not registered in the default profile and is used only by the + * {@code demo} profile. Provider usage, failures, missing usage, and concurrent + * dispatch waiting can all be reproduced deterministically, allowing the + * adapter and accounting lifecycle to be verified without an API key.

*/ public final class DemoChatModel implements ChatModel { diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoConfiguration.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoConfiguration.java index bfc4b71..bd4e35b 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoConfiguration.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoConfiguration.java @@ -8,7 +8,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -/** API key 없이 sample app을 실행하는 demo profile의 Spring AI 구성입니다. */ +/** Spring AI configuration for running the sample app in the demo profile without an API key. */ @Configuration(proxyBeanMethods = false) @Profile("demo") public class DemoConfiguration { diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoController.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoController.java index a452354..d557e32 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoController.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoController.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.Map; -/** {@code demo} 프로필에서만 노출되는 검증용 HTTP API입니다. */ +/** Verification HTTP API exposed only in the {@code demo} profile. */ @RestController @Profile("demo") public class DemoController { diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoRunReport.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoRunReport.java index 10bcc7e..e28b97e 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoRunReport.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoRunReport.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.Collections; -/** 모든 핵심 control/accounting 시나리오를 한 번에 실행한 demo 결과입니다. */ +/** Demo result containing all core control/accounting scenarios run together. */ public record DemoRunReport( String runId, String profile, diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioResult.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioResult.java index a54dbdf..46d3b32 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioResult.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioResult.java @@ -4,7 +4,7 @@ import java.util.Map; import java.util.Collections; -/** 화면에서 바로 읽을 수 있는 단일 demo 시나리오 결과입니다. */ +/** Single demo scenario result that can be read directly on screen. */ public record DemoScenarioResult( String scenario, String status, diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioService.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioService.java index 305309f..ad3d56d 100644 --- a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioService.java +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/DemoScenarioService.java @@ -45,8 +45,9 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * 반복 실행할 수 있는 framework-independent control/accounting 시나리오입니다. - * 모든 provider 호출은 {@link DemoChatModel}로만 전달되며 외부 네트워크를 사용하지 않습니다. + * Repeatable framework-independent control/accounting scenarios. + * All provider calls are routed only to {@link DemoChatModel}, and no external + * network is used. */ @Service @Profile("demo") @@ -226,7 +227,7 @@ private DemoScenarioResult budgetConcurrency(String runId) { )); } - // 경쟁 요청들은 첫 예약이 IN_FLIGHT인 동안 예약 단계에서 종료되어야 합니다. + // Competing requests must finish at the reservation stage while the first reservation is IN_FLIGHT. for (Future competitor : competitors) { competitorOutcomes.add(await(competitor)); } diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeController.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeController.java new file mode 100644 index 0000000..145eb6b --- /dev/null +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeController.java @@ -0,0 +1,268 @@ +package io.tokenpilot.sample; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.tokenpilot.autoconfigure.TokenPilotProperties; +import io.tokenpilot.budget.BudgetDecision; +import io.tokenpilot.budget.BudgetEvaluator; +import io.tokenpilot.budget.BudgetSnapshot; +import io.tokenpilot.budget.BudgetStateStore; +import io.tokenpilot.core.domain.TokenUsage; +import io.tokenpilot.core.domain.UsageSource; +import io.tokenpilot.springai.UsageExtractor; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.ChatClientResponse; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.context.annotation.Profile; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.UUID; + +/** Verification API that performs real provider calls only in the {@code openai-smoke} profile. */ +@RestController +@Profile("openai-smoke") +public final class OpenAiSmokeController { + + private static final String PROFILE = "openai-smoke"; + private static final String TENANT_ID = "openai-smoke"; + private static final String DEFAULT_PROMPT = + "Reply with exactly one short sentence confirming that the provider is reachable."; + + private final ChatClient.Builder chatClientBuilder; + private final UsageExtractor usageExtractor; + private final BudgetEvaluator budgetEvaluator; + private final BudgetStateStore budgetStateStore; + private final MeterRegistry meterRegistry; + private final TokenPilotProperties properties; + + public OpenAiSmokeController( + ChatClient.Builder chatClientBuilder, + UsageExtractor usageExtractor, + BudgetEvaluator budgetEvaluator, + BudgetStateStore budgetStateStore, + MeterRegistry meterRegistry, + TokenPilotProperties properties + ) { + this.chatClientBuilder = chatClientBuilder; + this.usageExtractor = usageExtractor; + this.budgetEvaluator = budgetEvaluator; + this.budgetStateStore = budgetStateStore; + this.meterRegistry = meterRegistry; + this.properties = properties; + } + + @GetMapping("/test/token-pilot/openai-smoke") + public OpenAiSmokeReport smoke( + @RequestParam(defaultValue = DEFAULT_PROMPT) String prompt + ) { + String normalizedPrompt = requirePrompt(prompt); + String modelId = requireModelId(); + int reservedOutputTokens = reservedOutputTokens(); + String requestId = "openai-smoke-" + UUID.randomUUID(); + + Map tags = Map.of("tenant_id", TENANT_ID); + BudgetDecision beforeDecision = budgetEvaluator.evaluate(tags); + BudgetSnapshot beforeBudget = budgetStateStore.snapshot( + beforeDecision.key(), + beforeDecision.limit() + ); + double beforeCommittedCost = counter( + "tokenpilot.cost.total", + "currency", "USD" + ); + double beforeCommitted = committedReconciliationCounter(); + double beforeReconciliationRequired = reconciliationRequiredCounter(); + double beforeReservationsCreated = counter( + "tokenpilot.budget.reservations", + "state", "created" + ); + + ChatClientResponse response = chatClientBuilder.clone() + .build() + .prompt() + .user(normalizedPrompt) + .options(ChatOptions.builder() + .model(modelId) + .maxTokens(reservedOutputTokens) + .temperature(0.0)) + .advisors(advisors -> advisors + .param("tenant_id", TENANT_ID) + .param("tokenpilot.request.id", requestId) + .param("tokenpilot.attempt.id", requestId + "-attempt")) + .call() + .chatClientResponse(); + + TokenUsage usage = usageExtractor.extract(response); + String responseModel = responseModel(response); + String responseText = responseText(response); + + BudgetDecision afterDecision = budgetEvaluator.evaluate(tags); + BudgetSnapshot afterBudget = budgetStateStore.snapshot( + afterDecision.key(), + afterDecision.limit() + ); + double committedCostDelta = counter( + "tokenpilot.cost.total", + "currency", "USD" + ) - beforeCommittedCost; + double committedDelta = committedReconciliationCounter() - beforeCommitted; + double reconciliationRequiredDelta = reconciliationRequiredCounter() + - beforeReconciliationRequired; + String accountingState = accountingState( + committedDelta, + reconciliationRequiredDelta + ); + boolean passed = hasText(responseText) + && usage.source() != UsageSource.UNAVAILABLE + && "COMMITTED".equals(accountingState); + + return new OpenAiSmokeReport( + passed ? "PASS" : "FAIL", + PROFILE, + "OpenAI Chat Completions", + requestId, + modelId, + responseModel, + responseText, + usage.source().name(), + usage.inputTokens(), + usage.outputTokens(), + usage.totalTokens(), + accountingState, + Map.of( + "committedCostUsd", committedCostDelta, + "reconciliationCommitted", committedDelta, + "reconciliationRequired", reconciliationRequiredDelta, + "reservationsCreated", + counter("tokenpilot.budget.reservations", "state", "created") + - beforeReservationsCreated + ), + budget(afterBudget), + note(passed, responseModel, usage) + ); + } + + private String requirePrompt(String prompt) { + if (!hasText(prompt)) { + throw new IllegalArgumentException("prompt must not be blank"); + } + return prompt.trim(); + } + + private String requireModelId() { + String modelId = properties.getSpringAi().getDefaultModelId(); + if (!hasText(modelId)) { + throw new IllegalStateException( + "token-pilot.spring-ai.default-model-id must be configured" + ); + } + return modelId.trim(); + } + + private int reservedOutputTokens() { + Long configured = properties.getSpringAi().getDefaultReservedOutputTokens(); + long value = configured == null ? 64L : configured; + if (value <= 0 || value > Integer.MAX_VALUE) { + throw new IllegalStateException( + "token-pilot.spring-ai.default-reserved-output-tokens must be between 1 and " + + Integer.MAX_VALUE + ); + } + return (int) value; + } + + private String responseModel(ChatClientResponse response) { + ChatResponse chatResponse = response == null ? null : response.chatResponse(); + return chatResponse == null || chatResponse.getMetadata() == null + ? null + : chatResponse.getMetadata().getModel(); + } + + private String responseText(ChatClientResponse response) { + ChatResponse chatResponse = response == null ? null : response.chatResponse(); + if (chatResponse == null) { + return null; + } + Generation result = chatResponse.getResult(); + if (result == null || result.getOutput() == null) { + return null; + } + return result.getOutput().getText(); + } + + private Map budget(BudgetSnapshot snapshot) { + return Map.of( + "committedCost", CostBoundaryFormatter.format(snapshot.committedCost()), + "activeReservedCost", CostBoundaryFormatter.format(snapshot.activeReservedCost()), + "pendingReconciliationLiability", + CostBoundaryFormatter.format(snapshot.pendingReconciliationLiability()), + "effectiveUsage", CostBoundaryFormatter.format(snapshot.effectiveUsage()), + "remaining", CostBoundaryFormatter.format(snapshot.remaining()), + "limit", CostBoundaryFormatter.format(snapshot.limit()) + ); + } + + private double committedReconciliationCounter() { + return counter( + "tokenpilot.reconciliation.outcomes", + "outcome", "committed", + "reason", "actual_usage_reported" + ); + } + + private double reconciliationRequiredCounter() { + return counter( + "tokenpilot.reconciliation.outcomes", + "outcome", "reconciliation_required", + "reason", "actual_usage_unavailable" + ) + counter( + "tokenpilot.reconciliation.outcomes", + "outcome", "reconciliation_required", + "reason", "pricing_reconciliation_required" + ); + } + + private double counter(String name, String... tags) { + Counter counter = meterRegistry.find(name).tags(tags).counter(); + return counter == null ? 0.0 : counter.count(); + } + + private String accountingState( + double committedDelta, + double reconciliationRequiredDelta + ) { + if (committedDelta > 0.0) { + return "COMMITTED"; + } + if (reconciliationRequiredDelta > 0.0) { + return "RECONCILIATION_REQUIRED"; + } + return "UNKNOWN"; + } + + private String note( + boolean passed, + String responseModel, + TokenUsage usage + ) { + if (passed) { + return "실제 OpenAI 응답의 usage를 정규화하고 예약 비용을 COMMITTED로 정산했습니다."; + } + if (usage.source() == UsageSource.UNAVAILABLE) { + return "provider 응답은 받았지만 usage를 확인하지 못해 정산 대기로 남았습니다."; + } + if (!hasText(responseModel)) { + return "provider 응답의 model metadata가 없어 정산을 확정하지 못했습니다."; + } + return "provider 응답은 받았지만 예약 snapshot과 actual 정산 결과를 확인해야 합니다."; + } + + private boolean hasText(String value) { + return value != null && !value.isBlank(); + } +} diff --git a/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeReport.java b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeReport.java new file mode 100644 index 0000000..d4aaf18 --- /dev/null +++ b/token-pilot-sample-app/src/main/java/io/tokenpilot/sample/OpenAiSmokeReport.java @@ -0,0 +1,30 @@ +package io.tokenpilot.sample; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** Smoke result showing the real OpenAI call and the corresponding Token Pilot settlement. */ +public record OpenAiSmokeReport( + String status, + String profile, + String provider, + String requestId, + String configuredModel, + String responseModel, + String response, + String usageSource, + long inputTokens, + long outputTokens, + long totalTokens, + String accountingState, + Map metrics, + Map budget, + String note +) { + + public OpenAiSmokeReport { + metrics = Collections.unmodifiableMap(new LinkedHashMap<>(metrics)); + budget = Collections.unmodifiableMap(new LinkedHashMap<>(budget)); + } +} diff --git a/token-pilot-sample-app/src/main/resources/application-openai-smoke.yml b/token-pilot-sample-app/src/main/resources/application-openai-smoke.yml new file mode 100644 index 0000000..e11c63e --- /dev/null +++ b/token-pilot-sample-app/src/main/resources/application-openai-smoke.yml @@ -0,0 +1,56 @@ +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: prometheus, health + metrics: + tags: + application: token-pilot-sample-app + +spring: + ai: + model: + chat: openai + embedding: none + image: none + audio: + transcription: none + speech: none + moderation: none + openai: + api-key: ${OPENAI_API_KEY} + timeout: 30s + max-retries: 0 + chat: + model: ${OPENAI_MODEL:gpt-4o-mini-2024-07-18} + temperature: 0.0 + +token-pilot: + enabled: true + pricing: + plans: + - model-id: gpt-4o-mini-2024-07-18 + currency: USD + rates: + PROMPT: ${OPENAI_PROMPT_PRICE_PER_1K:0.00015} + COMPLETION: ${OPENAI_COMPLETION_PRICE_PER_1K:0.00060} + spring-ai: + default-model-id: ${OPENAI_MODEL:gpt-4o-mini-2024-07-18} + default-reserved-output-tokens: 64 + framing-headroom-tokens: 0 + budget: + enabled: true + policy-id: openai-smoke-monthly + target-type: tenant + target-tag-key: tenant_id + fallback-target-id: openai-smoke + monthly-limit: ${OPENAI_SMOKE_MONTHLY_LIMIT:1.00} + currency: USD + zone-id: UTC + metrics: + enabled: true + legacy-ai-token-metrics-enabled: false + tag-whitelist: [] diff --git a/token-pilot-sample-app/src/main/resources/application.yml b/token-pilot-sample-app/src/main/resources/application.yml index 53589fd..0e0d18d 100644 --- a/token-pilot-sample-app/src/main/resources/application.yml +++ b/token-pilot-sample-app/src/main/resources/application.yml @@ -1,6 +1,17 @@ server: port: 8080 +spring: + ai: + model: + chat: none + embedding: none + image: none + audio: + transcription: none + speech: none + moderation: none + management: endpoints: web: diff --git a/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/DemoScenarioE2ETest.java b/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/DemoScenarioE2ETest.java index 5e880d6..63431c6 100644 --- a/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/DemoScenarioE2ETest.java +++ b/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/DemoScenarioE2ETest.java @@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThat; -/** API key 없이 실행되는 제출용 demo profile의 HTTP 계약을 검증합니다. */ +/** Verifies the HTTP contract of the submission demo profile without an API key. */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("demo") class DemoScenarioE2ETest { diff --git a/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/OpenAiSmokeE2ETest.java b/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/OpenAiSmokeE2ETest.java new file mode 100644 index 0000000..a937ef1 --- /dev/null +++ b/token-pilot-sample-app/src/test/java/io/tokenpilot/sample/OpenAiSmokeE2ETest.java @@ -0,0 +1,62 @@ +package io.tokenpilot.sample; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +import static org.assertj.core.api.Assertions.assertThat; + +/** Real OpenAI smoke test that can incur cost only when explicitly enabled. */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("openai-smoke") +@EnabledIfEnvironmentVariables({ + @EnabledIfEnvironmentVariable( + named = "RUN_OPENAI_SMOKE", + matches = "(?i)true", + disabledReason = "RUN_OPENAI_SMOKE=true일 때만 실제 provider를 호출합니다." + ), + @EnabledIfEnvironmentVariable( + named = "OPENAI_API_KEY", + matches = ".+", + disabledReason = "OPENAI_API_KEY가 없으면 실제 provider smoke를 건너뜁니다." + ) +}) +class OpenAiSmokeE2ETest { + + private final HttpClient httpClient = HttpClient.newHttpClient(); + + @LocalServerPort + private int port; + + @Test + void openAiResponseIsReconciledByTokenPilot() throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create( + "http://localhost:" + port + + "/test/token-pilot/openai-smoke?prompt=" + + "Reply%20with%20one%20short%20sentence." + )) + .GET() + .build(); + + HttpResponse response = httpClient.send( + request, + HttpResponse.BodyHandlers.ofString() + ); + + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.body()) + .contains("\"status\":\"PASS\"") + .contains("\"provider\":\"OpenAI Chat Completions\"") + .contains("\"accountingState\":\"COMMITTED\"") + .doesNotContain("\"usageSource\":\"UNAVAILABLE\""); + } +} diff --git a/token-pilot-spring-ai/build.gradle b/token-pilot-spring-ai/build.gradle index 0e4601d..ff48264 100644 --- a/token-pilot-spring-ai/build.gradle +++ b/token-pilot-spring-ai/build.gradle @@ -8,7 +8,7 @@ dependencies { api 'org.springframework.ai:spring-ai-model' api 'org.springframework.ai:spring-ai-client-chat' - // Reactor (스트리밍 처리를 위한 Flux 등) + // Reactor (for Flux and other streaming types) api 'io.projectreactor:reactor-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/LedgerAdvisor.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/LedgerAdvisor.java index 27421f6..6fb6e40 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/LedgerAdvisor.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/LedgerAdvisor.java @@ -7,8 +7,9 @@ import org.springframework.core.Ordered; /** - * ChatClient 호출 시 토큰 사용량을 가로채서 기록하는 어드바이저 인터페이스. - * Spring AI의 {@link BaseAdvisor}를 상속하여 AI 호출 전후 처리 로직을 표준 방식으로 정의합니다. + * Advisor interface that intercepts and records token usage during ChatClient calls. + * Extends Spring AI's {@link BaseAdvisor} to define standard processing before + * and after an AI call. */ public interface LedgerAdvisor extends BaseAdvisor { diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/ReservedOutputTokensResolver.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/ReservedOutputTokensResolver.java index 5c0910d..dad9d51 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/ReservedOutputTokensResolver.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/ReservedOutputTokensResolver.java @@ -4,12 +4,13 @@ import java.util.OptionalLong; -/** Provider 전용 request option에서 reserved output token 수를 해석합니다. */ +/** Resolves reserved output token counts from provider-specific request options. */ @FunctionalInterface public interface ReservedOutputTokensResolver { /** - * 지원하는 provider request이면 양수 token 수를, 지원하지 않으면 빈 값을 반환합니다. + * Returns a positive token count for a supported provider request, or empty + * when the request is not supported. */ OptionalLong resolve(ChatClientRequest request); } diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/UsageExtractor.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/UsageExtractor.java index 4aaabdf..078206a 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/UsageExtractor.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/UsageExtractor.java @@ -4,7 +4,8 @@ import org.springframework.ai.chat.client.ChatClientResponse; /** - * AI 응답({@link ChatClientResponse})에서 토큰 사용량 정보를 추출하는 인터페이스. + * Interface for extracting token usage information from an AI response + * ({@link ChatClientResponse}). */ public interface UsageExtractor { TokenUsage extract(ChatClientResponse response); diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java index e305d2c..326b9e2 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultLedgerAdvisor.java @@ -37,16 +37,16 @@ import org.springframework.ai.chat.prompt.ChatOptions; /** - * 기본 {@link LedgerAdvisor} 구현체. - * {@link UsageExtractor}를 사용하여 토큰 사용량을 추출하고, - * 그 결과를 {@link LedgerManager}에 기록하는 핵심 비즈니스 로직을 수행합니다. - * 또한 {@link BudgetEvaluator}를 통해 예산 초과 여부를 사전에 차단하고, - * 호출 성공 시 {@link BudgetStateStore}에 비용을 누적합니다. + * Default {@link LedgerAdvisor} implementation. + * Uses {@link UsageExtractor} to extract token usage and records the result in + * {@link LedgerManager} as its core business logic. It also blocks budget + * overages in advance through {@link BudgetEvaluator} and accumulates cost in + * {@link BudgetStateStore} when a call succeeds. *

- * {@link #before(ChatClientRequest, AdvisorChain)}는 budget-disabled ledger-only - * compatibility 경로의 상태 guard입니다. budget-enabled 호출은 - * {@link #adviseCall(ChatClientRequest, CallAdvisorChain)}에서 후보 비용을 포함한 - * preflight와 원자적 reservation lifecycle을 수행합니다. + * {@link #before(ChatClientRequest, AdvisorChain)} is the state guard for the + * budget-disabled ledger-only compatibility path. Budget-enabled calls perform + * preflight with candidate cost and the atomic reservation lifecycle in + * {@link #adviseCall(ChatClientRequest, CallAdvisorChain)}. */ public class DefaultLedgerAdvisor implements LedgerAdvisor { diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultUsageExtractor.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultUsageExtractor.java index f3d1c3b..3502a55 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultUsageExtractor.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/DefaultUsageExtractor.java @@ -16,10 +16,11 @@ import java.util.Map; /** - * 기본 {@link UsageExtractor} 구현체. - * Spring AI의 {@link Usage} 정보를 {@link TokenUsage}로 변환하며, - * provider native usage와 메타데이터에서 cache/reasoning breakdown을 식별합니다. - * Provider가 비포괄 총량을 반환하면 Token Pilot의 포괄 총량 계약에 맞게 정규화합니다. + * Default {@link UsageExtractor} implementation. + * Converts Spring AI {@link Usage} information to {@link TokenUsage} and + * identifies cache/reasoning breakdowns in provider-native usage and metadata. + * When a provider returns non-inclusive totals, normalizes them to Token Pilot's + * inclusive-total contract. */ public class DefaultUsageExtractor implements UsageExtractor { private static final TypeReference> MAP_TYPE = new TypeReference<>() {}; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/IdempotencyKeyResolver.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/IdempotencyKeyResolver.java index 06a1951..a5d55a0 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/IdempotencyKeyResolver.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/IdempotencyKeyResolver.java @@ -6,7 +6,7 @@ import java.util.Objects; import java.util.function.Supplier; -/** 현재 Advisor 호출에서 사용할 idempotency key를 결정합니다. */ +/** Determines the idempotency key for the current advisor call. */ final class IdempotencyKeyResolver { private final RequestContextAccessor contextAccessor; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java index 591907c..368c7dd 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/LedgerSpringAiComponents.java @@ -26,7 +26,7 @@ import java.util.Objects; /** - * Spring AI 어댑터 컴포넌트 생성을 위한 팩토리 클래스입니다. + * Factory for creating Spring AI adapter components. */ public final class LedgerSpringAiComponents { diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ModelResolver.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ModelResolver.java index 03ec39a..80e6e94 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ModelResolver.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ModelResolver.java @@ -9,7 +9,7 @@ import java.util.Objects; import java.util.Optional; -/** request model을 configured default 순서로 선택하고 Core registry에서 해석합니다. */ +/** Selects the request model before the configured default and resolves it in the Core registry. */ final class ModelResolver { private final ModelRegistry modelRegistry; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestContextAccessor.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestContextAccessor.java index 5e4145f..20ae4b5 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestContextAccessor.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestContextAccessor.java @@ -7,7 +7,7 @@ import java.util.Objects; -/** Spring AI request context에서 Token Pilot correlation 값을 읽고 추가합니다. */ +/** Reads and adds Token Pilot correlation values in the Spring AI request context. */ final class RequestContextAccessor { static final String REQUEST_ID_CONTEXT_KEY = "tokenpilot.request.id"; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestMessageRole.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestMessageRole.java index 8c295fb..98f7d77 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestMessageRole.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestMessageRole.java @@ -1,6 +1,6 @@ package io.tokenpilot.springai.internal; -/** Token Pilot이 지원하는 text message 타입입니다. */ +/** Text message types supported by Token Pilot. */ enum RequestMessageRole { SYSTEM, USER, diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestPreflight.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestPreflight.java index 0959291..1ec0e3a 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestPreflight.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestPreflight.java @@ -18,7 +18,7 @@ import java.util.Objects; -/** Spring AI 요청 해석 결과를 기존 Core preflight 계약에 연결합니다. */ +/** Connects Spring AI request resolution results to the existing Core preflight contract. */ final class RequestPreflight { private final DefaultRequestAdapter requestAdapter = new DefaultRequestAdapter(); diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResolver.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResolver.java index ae283f9..a9c4900 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResolver.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResolver.java @@ -14,7 +14,7 @@ import java.util.Objects; -/** Spring AI 요청이 Token Pilot 0.1.0에서 지원하는 범위인지 판정합니다. */ +/** Determines whether a Spring AI request is within the scope supported by Token Pilot 0.1.0. */ final class RequestScopeResolver { RequestScopeResult resolve(ChatClientRequest request) { diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResult.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResult.java index 5aa3df2..7979236 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResult.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/RequestScopeResult.java @@ -5,7 +5,7 @@ import java.util.Objects; import java.util.Optional; -/** 지원하는 request scope 또는 제한된 미지원 사유를 나타냅니다. */ +/** Represents a supported request scope or a bounded unsupported reason. */ final class RequestScopeResult { private final Optional scope; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ReservedOutputResolver.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ReservedOutputResolver.java index b97ccc4..79f2145 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ReservedOutputResolver.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/ReservedOutputResolver.java @@ -8,7 +8,7 @@ import java.util.Objects; import java.util.OptionalLong; -/** request maxTokens를 configured default 순서로 해석합니다. */ +/** Resolves request maxTokens before the configured default. */ final class ReservedOutputResolver { private final @Nullable ReservedOutputTokensResolver providerResolver; diff --git a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/UnsupportedRequestScopeReason.java b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/UnsupportedRequestScopeReason.java index 8592577..6550fa6 100644 --- a/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/UnsupportedRequestScopeReason.java +++ b/token-pilot-spring-ai/src/main/java/io/tokenpilot/springai/internal/UnsupportedRequestScopeReason.java @@ -1,6 +1,6 @@ package io.tokenpilot.springai.internal; -/** Token Pilot 0.1.0이 지원하지 않는 Spring AI request scope의 제한된 사유입니다. */ +/** Bounded reasons for Spring AI request scopes unsupported by Token Pilot 0.1.0. */ enum UnsupportedRequestScopeReason { TOOL_MESSAGE, MEDIA,