adachsoft / llm-cost-calculator
Framework-agnostic calculator of LLM usage cost based on a component-based pricing model.
Requires
- php: >=8.3
- ext-bcmath: *
- adachsoft/ai-model-list: ^0.5
- adachsoft/collection: ^3.0
Requires (Dev)
- adachsoft/php-code-style: ^0.5.0
- friendsofphp/php-cs-fixer: ^3.95
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.3
- rector/rector: ^2.6
- symplify/phpstan-rules: ^14.12
This package is not auto-updated.
Last update: 2026-08-18 13:13:30 UTC
README
Framework-agnostic calculator of LLM usage cost based on a component-based pricing model.
This library is purely computational: it performs no I/O, no HTTP calls, and has no knowledge of LLM providers. You pass usage quantities and a pricing catalog; it returns the calculated cost.
Requirements
- PHP
>= 8.3 - PHP extension
ext-bcmath adachsoft/ai-model-list^0.5(pricing DTO types only)adachsoft/collection^3.0
Installation
composer require adachsoft/llm-cost-calculator
Quick start
<?php
declare(strict_types=1);
use AdachSoft\AIModelList\PublicApi\Collection\PriceComponentDtoCollection;
use AdachSoft\AIModelList\PublicApi\Collection\PriceConditionDtoCollection;
use AdachSoft\AIModelList\PublicApi\Dto\ModelPricingDto;
use AdachSoft\AIModelList\PublicApi\Dto\PriceComponentDto;
use AdachSoft\LlmCostCalculator\PublicApi\Collection\UsageItemDtoCollection;
use AdachSoft\LlmCostCalculator\PublicApi\Dto\PricingContextDto;
use AdachSoft\LlmCostCalculator\PublicApi\Dto\UsageDto;
use AdachSoft\LlmCostCalculator\PublicApi\Dto\UsageItemDto;
use AdachSoft\LlmCostCalculator\PublicApi\Factory\CostCalculatorFactory;
$calculator = CostCalculatorFactory::create();
$usage = new UsageDto(
new UsageItemDtoCollection([
new UsageItemDto('input', 1_000_000),
new UsageItemDto('output', 500_000),
]),
modelId: 'gpt-example',
providerId: 'example-provider',
);
// In a real application, ModelPricingDto usually comes from adachsoft/ai-model-list.
$pricing = new ModelPricingDto(new PriceComponentDtoCollection([
new PriceComponentDto(
kind: 'input',
unit: 'per_million_tokens',
amount: '3.00',
currency: 'USD',
conditions: new PriceConditionDtoCollection(),
),
new PriceComponentDto(
kind: 'output',
unit: 'per_million_tokens',
amount: '15.00',
currency: 'USD',
conditions: new PriceConditionDtoCollection(),
),
]));
$result = $calculator->calculate(
$usage,
$pricing,
PricingContextDto::createEmpty(),
);
echo $result->total->amount; // e.g. "10.500000000000"
echo $result->total->currency; // "USD"
if (!$result->isComplete()) {
// IMPORTANT: total is understated — some usage kinds had no matching price.
// Do not use the amount for billing until unpriced kinds are resolved.
print_r($result->unpricedKinds);
}
if ($result->hasBreakdown()) {
foreach ($result->components as $component) {
// kind, quantity, unit, unitPrice, cost, appliedConditions
}
}
Important: unpricedKinds and incomplete totals
When a usage kind has no matching price component, the calculator continues and records that kind in CostResultDto::$unpricedKinds.
- A non-empty
unpricedKindslist means the total is understated. - Always call
isComplete()before using the amount for billing or accounting. - Prefer inspecting the breakdown (
hasBreakdown()/$components) for audits and optimization.
Decimal precision (truncation, not rounding)
Arithmetic uses ext-bcmath with a configurable scale (default: 12 decimal places via CostCalculatorFactory::DEFAULT_SCALE).
Results are truncated to the configured scale, not rounded. At scale 12 the truncation error is far smaller than any practical billing amount, and truncation is fully deterministic.
Extending units
Default units and divisors:
| Unit | Divisor |
|---|---|
per_million_tokens | 1000000 |
per_thousand_tokens | 1000 |
per_token | 1 |
per_request | 1 |
per_image | 1 |
per_second | 1 |
Add or override units without changing library code:
$calculator = CostCalculatorFactory::createWithUnits([
'per_minute' => '1',
// overrides are allowed:
// 'per_request' => '5',
]);
For full control (custom math, matcher, or resolver), use CostCalculatorFactory::createWithDependencies(...).
Conventional kind and unit values
These values are conventions only. Both sets are open — any ^[a-z0-9_]+$ string is valid.
Common kinds: input, output, cache_read, cache_write, reasoning, request, image, audio_input, audio_output
Common units: per_million_tokens, per_thousand_tokens, per_token, per_request, per_image, per_second
New kinds or units require no library code changes — only data (and optionally a unit divisor via createWithUnits()).
Pricing conditions
Price components may carry conditions (tier, batch mode, region, etc.). Pass matching facts through PricingContextDto. The most specific matching component wins (largest number of satisfied conditions). Components with empty conditions are base rates.
Public API surface
- Create calculator:
AdachSoft\LlmCostCalculator\PublicApi\Factory\CostCalculatorFactory - Contract:
AdachSoft\LlmCostCalculator\PublicApi\CostCalculatorInterface - Input/output DTOs and VO: under
PublicApi\Dto,PublicApi\Collection,PublicApi\Vo - Exceptions: all extend
PublicApi\Exception\CostCalculationException
Internal implementations are marked @internal and should not be instantiated directly by consumers.
Architecture decisions
Stable design decisions are recorded as ADRs in docs/adr/:
- Decimal money via BCMath (no float)
- Open kind/unit model and condition matching
- Public API vs Internal and ai-model-list dependency
Development
composer install
composer test
composer phpstan
composer phpcsfix
License
MIT