PHPackages                             lunetics/llm-cost-tracking-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. lunetics/llm-cost-tracking-bundle

ActiveSymfony-bundle[Debugging &amp; Profiling](/categories/debugging)

lunetics/llm-cost-tracking-bundle
=================================

Symfony bundle that tracks LLM API costs and displays them in the Web Debug Toolbar and Profiler

v0.4.0(4w ago)4511MITPHPPHP &gt;=8.2CI passing

Since Feb 25Pushed 4w agoCompare

[ Source](https://github.com/lunetics/LuneticsLlmCostTrackingBundle)[ Packagist](https://packagist.org/packages/lunetics/llm-cost-tracking-bundle)[ Docs](https://github.com/lunetics/llm-cost-tracking-bundle)[ RSS](/packages/lunetics-llm-cost-tracking-bundle/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (9)Dependencies (34)Versions (15)Used By (1)

LuneticsLlmCostTrackingBundle
=============================

[](#luneticsllmcosttrackingbundle)

[![CI](https://github.com/lunetics/llm-cost-tracking-bundle/actions/workflows/ci.yml/badge.svg)](https://github.com/lunetics/llm-cost-tracking-bundle/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/64c79574323706fa7924cd8b565ff4f49c29fabd8c34a124bc2e476cd37e9dad/68747470733a2f2f636f6465636f762e696f2f67682f6c756e65746963732f6c6c6d2d636f73742d747261636b696e672d62756e646c652f67726170682f62616467652e737667)](https://codecov.io/gh/lunetics/llm-cost-tracking-bundle)[![Latest Stable Version](https://camo.githubusercontent.com/e67da180f89faf4a6f33b70940e1efb67e6d71b487a93120fb110dc7b9fd8414/68747470733a2f2f706f7365722e707567782e6f72672f6c756e65746963732f6c6c6d2d636f73742d747261636b696e672d62756e646c652f762f737461626c65)](https://packagist.org/packages/lunetics/llm-cost-tracking-bundle)[![Total Downloads](https://camo.githubusercontent.com/dfd067f4994e746f1f06e2080018069d09327cd11b9aa47c3bde096269e0d8e5/68747470733a2f2f706f7365722e707567782e6f72672f6c756e65746963732f6c6c6d2d636f73742d747261636b696e672d62756e646c652f646f776e6c6f616473)](https://packagist.org/packages/lunetics/llm-cost-tracking-bundle)[![License](https://camo.githubusercontent.com/30d245f98f6c69c4a42ef30c73cf23e18dd38d54eb7defdc7c3c1e99b97657db/68747470733a2f2f706f7365722e707567782e6f72672f6c756e65746963732f6c6c6d2d636f73742d747261636b696e672d62756e646c652f6c6963656e7365)](https://packagist.org/packages/lunetics/llm-cost-tracking-bundle)

A Symfony bundle that tracks LLM API costs and displays them in the Web Debug Toolbar and Profiler.

Hooks into [symfony/ai-bundle](https://github.com/symfony/ai-bundle)'s `TraceablePlatform` to calculate per-request costs based on token usage, with support for input, output, cached, and thinking tokens.

Requirements
------------

[](#requirements)

- PHP &gt;= 8.2
- Symfony &gt;= 7.0
- symfony/ai-bundle &gt;= 0.4

Installation
------------

[](#installation)

```
composer require lunetics/llm-cost-tracking-bundle
```

If you are not using [Symfony Flex](https://github.com/symfony/flex), register the bundle manually:

```
// config/bundles.php
return [
    // ...
    Lunetics\LlmCostTrackingBundle\LuneticsLlmCostTrackingBundle::class => ['all' => true],
];
```

Features
--------

[](#features)

- **Web Debug Toolbar** — shows total cost and call count for the current request
- **Profiler Panel** — per-call breakdown with model, tokens, and cost
- **Per-model aggregation** — costs grouped by model with totals
- **Budget warnings** — toolbar alerts when costs exceed a configurable threshold
- **Color-coded costs** — green/yellow/red based on configurable thresholds
- **Dynamic pricing** — automatically fetches live model pricing from [models.dev](https://models.dev), covering hundreds of models without any manual configuration
- **Unconfigured model detection** — warns when a model has no pricing data
- **Injectable cost tracking** — use `CostTrackerInterface` in your own services to access cost data outside the profiler
- **Extensible** — implement `CostCalculatorInterface` to provide custom pricing logic

Model Pricing
-------------

[](#model-pricing)

The bundle resolves pricing for a model using the following priority order:

1. **Your YAML config** — `models:` entries take precedence over everything else
2. **Dynamic pricing from models.dev** — the bundle fetches live pricing from [models.dev](https://models.dev) (thousands of models) and caches it for 24 hours. When models.dev is unreachable, a bundled snapshot of ~3000 models is used as a fallback so known models are still priced correctly during outages
3. **Not found** — cost is shown as zero with a warning in the profiler

This means most models work out of the box with no configuration. Your own entries always win.

> **All prices are in USD.** The models.dev feed and any prices you configure are all treated as USD. There is no currency conversion; the `$` prefix shown in the profiler is a literal dollar sign.

Configuration
-------------

[](#configuration)

```
# config/packages/lunetics_llm_cost_tracking.yaml
lunetics_llm_cost_tracking:
    budget_warning: 0.50          # toolbar turns red when exceeded
    cost_thresholds:
        low: 0.01                 # below = green
        medium: 0.10              # between low/medium = yellow, above = red
    logging:
        enabled: false            # default: false — opt in to log per-request cost data via Monolog
        channel: 'ai'             # Monolog channel (default: 'ai'); route it via your handlers
    dynamic_pricing:
        enabled: true             # default: true — fetch live pricing from models.dev
        ttl: 86400                # cache duration in seconds (default: 24h, max: 7 days)
    models:
        my-custom-model:
            display_name: 'My Custom Model'
            provider: 'MyProvider'
            input_price_per_million: 1.00
            output_price_per_million: 5.00
            cached_input_price_per_million: 0.10   # optional
            thinking_price_per_million: 5.00       # optional
```

### Logging

[](#logging)

Logging is **disabled by default**. Enable it explicitly to have per-request LLM cost data written via Monolog:

```
lunetics_llm_cost_tracking:
    logging:
        enabled: true
```

Each request that makes at least one AI call produces:

- one `info` log per call (model, provider, token breakdown, cost)
- one `info` summary log (total calls, cost, tokens)
- one `warning` if any models lack pricing configuration

Logs are emitted on `kernel.terminate` — after the response is sent — so there is no latency impact.

To route AI cost logs to a dedicated file, configure a Monolog handler for the `ai` channel:

```
# config/packages/monolog.yaml
monolog:
    channels: [ai]
    handlers:
        ai_costs:
            type: stream
            path: '%kernel.logs_dir%/ai_costs.log'
            channels: [ai]
```

If the `ai` channel is not explicitly configured, logs fall through to your default handler.

To use a different channel name:

```
lunetics_llm_cost_tracking:
    logging:
        channel: 'llm'
```

### Disabling Dynamic Pricing

[](#disabling-dynamic-pricing)

If you want fully offline/air-gapped operation, or prefer explicit control over every model's price:

```
lunetics_llm_cost_tracking:
    dynamic_pricing:
        enabled: false
```

When disabled, the bundled snapshot continues to provide model coverage as a read-only baseline — no live HTTP requests are made, and the `lunetics:llm:update-pricing` command is removed from the container. Your explicit `models:` config always takes priority over the snapshot. This is the right choice for air-gapped or reproducible-pricing environments where you want stable costs without live API calls.

> **Upgrade note (v0.3):** The old static list of bundled defaults has been replaced with a versioned pricing snapshot (`resources/pricing_snapshot.json`). When `dynamic_pricing.enabled: false`, the snapshot serves as an always-on baseline — models that previously returned `null` pricing may now resolve from the snapshot. If you need strict "only my config" behaviour where unknown models always return nothing, add `models:` entries for every model you use and configure your app to treat unresolved models as an error.

### Adjusting the Cache TTL

[](#adjusting-the-cache-ttl)

The dynamic pricing response is cached to avoid unnecessary HTTP requests on every page load. The default TTL is 24 hours. To refresh more or less frequently:

```
lunetics_llm_cost_tracking:
    dynamic_pricing:
        ttl: 3600    # 1 hour
```

Minimum: 1 second. Maximum: 604800 (7 days).

Console Command
---------------

[](#console-command)

To manually refresh the cached pricing from models.dev:

```
php bin/console lunetics:llm:update-pricing
```

This clears the cache and immediately fetches fresh pricing. Add `--verbose` to see the full model table:

```
php bin/console lunetics:llm:update-pricing --verbose
```

The command exits with a non-zero status if the API is unreachable or returns no models, making it safe to use in deployment pipelines.

Model Coverage
--------------

[](#model-coverage)

The bundle ships a versioned snapshot of the [models.dev](https://models.dev) catalogue (~3000 models across ~100 providers) as `resources/pricing_snapshot.json`. This snapshot is used as a fallback when the live API is unreachable, so pricing works correctly even in offline or air-gapped environments.

The snapshot is regenerated before each release. To regenerate it locally:

```
make update-snapshot
# or directly:
php bin/generate_snapshot.php
```

The model string passed to `$platform->invoke()` (e.g. `'gpt-5'`) is the same string the bundle uses to look up pricing.

### Example: OpenAI GPT

[](#example-openai-gpt)

```
# config/packages/symfony_ai.yaml
symfony_ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'
```

```
// The model string 'gpt-5' is matched against the pricing registry
$result = $platform->invoke('gpt-5', 'Explain Symfony in one sentence.');
```

No `lunetics_llm_cost_tracking` config is needed — `gpt-5` is covered by dynamic pricing (or the bundled snapshot when offline). Costs appear automatically in the profiler toolbar.

### Overriding or Adding Model Pricing

[](#overriding-or-adding-model-pricing)

If you use a model that isn't on models.dev (e.g. a fine-tuned or self-hosted model), add it to your config:

```
lunetics_llm_cost_tracking:
    models:
        ft:gpt-5:my-finetuned-2025:
            display_name: 'My Fine-tuned GPT-5'
            provider: 'OpenAI'
            input_price_per_million: 3.00
            output_price_per_million: 15.00
```

Your `models:` entries always take precedence over bundle defaults and dynamic pricing.

Using Cost Data in Your Services
--------------------------------

[](#using-cost-data-in-your-services)

The `CostTrackerInterface` service is available for dependency injection. Use it to access cost data outside the profiler — for example, in middleware, event listeners, or API responses:

```
use Lunetics\LlmCostTrackingBundle\Service\CostTrackerInterface;

class MyService
{
    public function __construct(
        private readonly CostTrackerInterface $costTracker,
    ) {}

    public function logCosts(): void
    {
        $totals = $this->costTracker->getTotals();
        // $totals = ['calls' => 3, 'input_tokens' => 5000, ..., 'cost' => 0.042]

        // Or get everything in one call:
        $snapshot = $this->costTracker->getSnapshot();
        // $snapshot = ['calls' => [...], 'by_model' => [...], 'totals' => [...], 'unconfigured_models' => [...]]
    }
}
```

Available methods: `getCalls()`, `getTotals()`, `getByModel()`, `getUnconfiguredModels()`, `getSnapshot()`.

The `ModelRegistryInterface` and `CostCalculatorInterface` are also available for injection if you need lower-level access to model definitions or cost calculation.

How It Works
------------

[](#how-it-works)

The bundle collects data from all services tagged with `ai.traceable_platform` (provided by symfony/ai-bundle). The `CostTracker` service iterates over all recorded LLM calls, extracts token usage metadata, and calculates costs using the configured model pricing. Results are memoized for the lifetime of the request, so repeated calls to any getter return the same data without recomputation.

The `LlmCostCollector` (Symfony Profiler data collector) delegates to `CostTracker` via `getSnapshot()`, which returns all cost data in a single atomic call. This separation keeps business logic in a standalone service that can be injected anywhere, while the data collector focuses on profiler integration.

Cost formula per call:

```
cost = (regular_input_tokens / 1M × input_price)
     + (output_tokens / 1M × output_price)
     + (cached_tokens / 1M × cached_price)
     + (thinking_tokens / 1M × thinking_price)

```

Where `regular_input_tokens = max(0, input_tokens - cached_tokens)`.

Development
-----------

[](#development)

A Docker-based Makefile is provided for local development:

```
make install          # Install dependencies
make test             # Run PHPUnit tests
make phpstan          # Run PHPStan (level 8)
make cs-check         # Check coding standards
make cs-fix           # Fix coding standards
make ci               # Run all checks
make update-snapshot  # Regenerate resources/pricing_snapshot.json from models.dev
```

Override the PHP version with `PHP_VERSION=8.2 make test`.

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance94

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~11 days

Recently: every ~23 days

Total

9

Last Release

28d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/149752?v=4)[Matthias Breddin](/maintainers/lunetics)[@lunetics](https://github.com/lunetics)

---

Top Contributors

[![lunetics](https://avatars.githubusercontent.com/u/149752?v=4)](https://github.com/lunetics "lunetics (54 commits)")

---

Tags

symfonyprofilertoolbaraitrackingllmcost

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/lunetics-llm-cost-tracking-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/lunetics-llm-cost-tracking-bundle/health.svg)](https://phpackages.com/packages/lunetics-llm-cost-tracking-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.5M374](/packages/easycorp-easyadmin-bundle)[sulu/sulu

Core framework that implements the functionality of the Sulu content management system

1.3k1.4M196](/packages/sulu-sulu)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

585.4M518](/packages/shopware-core)[chameleon-system/chameleon-base

The Chameleon System core.

1027.9k4](/packages/chameleon-system-chameleon-base)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1615.6k12](/packages/2lenet-crudit-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
