adachsoft/llm-cost-calculator

Framework-agnostic calculator of LLM usage cost based on a component-based pricing model.

Maintainers

Package info

gitlab.com/a.adach/llm-cost-calculator

Issues

pkg:composer/adachsoft/llm-cost-calculator

Transparency log

Statistics

Installs: 4

Dependents: 1

Suggesters: 0

Stars: 0

v0.1.0 2026-08-17 09:44 UTC

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 unpricedKinds list 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:

UnitDivisor
per_million_tokens1000000
per_thousand_tokens1000
per_token1
per_request1
per_image1
per_second1

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/:

Development

composer install
composer test
composer phpstan
composer phpcsfix

License

MIT