Estimate LLM API Cost per Request Before You Ship

Estimate LLM API cost per request from real token usage, not calculator guesses. PHP code to log usage, find p95 cost and set a hard ceiling.

Developer estimating LLM API cost per request using input and output tokens, token pricing, and request usage data.

Short answer: to estimate LLM API cost per request, multiply the input tokens by the provider’s input rate and the output tokens by its output rate, add them, and divide by one million if rates are quoted per million tokens. The formula is the easy part. The honest estimate comes from measuring real requests in staging, reading the usage object the API returns, and pricing from the median and the 95th percentile rather than from one guess typed into a calculator. Then use your output cap to turn that estimate into a hard ceiling you can put in a quote.

Every calculator on the first page of search results does the multiplication for you. None of them knows how long your system prompt is, how chatty your users are, or how often your code retries. Those three things decide the bill.

A note on numbers before we start: this article contains no prices. Per-token rates change, differ by model, and are published by your provider. Copy them from its pricing page on the day you estimate, and write that date next to the estimate.

The formula to estimate LLM API cost per request

For a single call:

cost = ( uncached_input_tokens × input_rate
       + cached_input_tokens   × cached_input_rate
       + output_tokens         × output_rate ) / 1,000,000

Three details make this formula match the invoice instead of undershooting it.

Input is everything you send, not just the user’s message. The system prompt, any instructions, any context you attach and, in a chat feature, the whole conversation so far. On a short question, the system prompt is often the biggest part of the input.

Output includes tokens you never see. Reasoning models think before they answer. OpenAI’s reasoning guide (read September 24, 2026) says reasoning tokens “are not visible via the API” but are “billed as output tokens.” A reply that looks like two sentences can carry far more billable output than two sentences.

Cached input is billed at its own rate. If the start of your prompt repeats across requests, some providers bill the repeated part at a reduced rate. OpenAI’s prompt caching guide (read the same day) says caching “is enabled by default for supported OpenAI models” and, for its newer models, lists cached reads at “0.1× the uncached input-token rate.” The multiplier depends on the model, so take it from your provider’s docs rather than from this page.

Why a calculator guess is not an estimate

A calculator asks for “average input tokens” and “average output tokens.” You do not know those numbers yet, and averages hide the requests that cost the most. Three things push real traffic away from the guess:

  • Your fixed overhead. A long system prompt is sent with every request and is easy to forget.
  • Output variance. Some answers are one line; some run to the cap. The cost distribution has a long tail.
  • Retries and repairs. A timeout that retries is a second paid request. A reply that fails JSON parsing and gets re-asked is another.

The fix is not a better guess. It is measurement.

Measure it: log the usage object from real requests

Every non-streaming response from a Chat Completions style API carries a usage object. OpenAI’s API reference for creating a chat completion (read September 24, 2026) lists prompt_tokens, completion_tokens and total_tokens, with prompt_tokens_details.cached_tokens and completion_tokens_details.reasoning_tokens as breakdowns. The Responses API reports the same ideas as input_tokens, output_tokens, input_tokens_details.cached_tokens and output_tokens_details.reasoning_tokens. Streaming responses report usage differently, so check your provider’s streaming docs before you rely on it there.

The breakdowns are parts of the totals, not additions to them. Cached tokens are part of the prompt count, and reasoning tokens are part of the completion count. Add them on top and you double-count.

Log one line per request, tagged with the feature that made it. This targets PHP 8.1 or later and assumes you already have the decoded response in $data:

<?php
// Call this right after a successful provider response.
function log_llm_usage(string $feature, string $model, array $usage): void
{
    $line = json_encode([
        'ts'        => date('c'),
        'feature'   => $feature,
        'model'     => $model,
        'input'     => $usage['prompt_tokens'] ?? 0,
        'cached'    => $usage['prompt_tokens_details']['cached_tokens'] ?? 0,
        'output'    => $usage['completion_tokens'] ?? 0,
        'reasoning' => $usage['completion_tokens_details']['reasoning_tokens'] ?? 0,
    ]);

    // Outside the web root, so the server never serves it.
    file_put_contents(
        __DIR__ . '/../var/llm-usage.jsonl',
        $line . PHP_EOL,
        FILE_APPEND | LOCK_EX
    );
}

log_llm_usage('support-reply', $model, $data['usage'] ?? []);

Keep that file out of the public folder for the same reason you keep a debug log there: anything under the web root can be downloaded, which is the logic behind running WP_DEBUG_LOG without displaying errors to visitors with the log moved somewhere private.

Then feed staging a realistic sample: real support questions with personal data removed, real documents, real conversation lengths. A few hundred requests per feature tells you far more than any average you could invent.

Turn the log into a cost report

Rates live in one config file you update by hand from the pricing page. The zeros below are placeholders, not prices:

<?php
// config/llm-rates.php
// Rates per million tokens, copied from your provider's pricing page.
// Record the date you copied them. Zeros are placeholders.
return [
    'your-model-id' => [
        'input'        => 0.0,
        'cached_input' => 0.0,
        'output'       => 0.0,
    ],
];

And a small CLI report that prints the median, the 95th percentile and the maximum cost per request for each feature:

<?php
// bin/llm-cost-report.php
// Run: php bin/llm-cost-report.php   (PHP 8.1+)
declare(strict_types=1);

function row_cost(array $row, array $rate): float
{
    $uncached = $row['input'] - $row['cached'];

    return ($uncached * $rate['input']
        + $row['cached'] * $rate['cached_input']
        + $row['output'] * $rate['output']) / 1_000_000;
}

function percentile(array $sorted, float $p): float
{
    $index = (int) ceil($p * count($sorted)) - 1;

    return $sorted[max(0, $index)];
}

$rates = require __DIR__ . '/../config/llm-rates.php';
$lines = file(__DIR__ . '/../var/llm-usage.jsonl', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
    exit("No usage log found.\n");
}

$byFeature = [];
foreach ($lines as $line) {
    $row = json_decode($line, true);
    if (!is_array($row) || !isset($rates[$row['model']])) {
        continue; // unknown model: add its rates before trusting the report
    }
    $byFeature[$row['feature']][] = row_cost($row, $rates[$row['model']]);
}

foreach ($byFeature as $feature => $costs) {
    sort($costs);
    printf(
        "%-20s n=%-6d median=%.6f p95=%.6f max=%.6f\n",
        $feature,
        count($costs),
        percentile($costs, 0.50),
        percentile($costs, 0.95),
        $costs[count($costs) - 1]
    );
}

The median tells you what a typical request costs. The 95th percentile tells you what to budget, because a month of traffic contains plenty of the expensive requests. The maximum tells you whether your caps are doing their job.

The trap in chat features: the fifth message costs more than the first

Most chat features resend the whole conversation with every turn, because the model has no memory between requests. That makes input grow with every exchange. If the system prompt is S tokens, each user message about U and each reply about A, then turn n sends roughly:

input(n) = S + n × U + (n - 1) × A

Here is what that looks like with illustrative token counts, not measurements: a system prompt of 800, user messages of 100 and replies of 300.

TurnInput tokens sent
1900
21,300
31,700
52,500
104,500

Across ten turns, those illustrative numbers add up to 27,000 input tokens, three times the 9,000 you would get by pricing every turn like the first. Growth like that is why a chat estimate must be per conversation, not per request. Two things bend the curve: capping how much history you resend, and keeping the stable part of the prompt at the front so caching can apply. OpenAI’s prompt caching guide recommends putting “stable developer instructions and shared reference material first” for exactly that reason.

From estimate to ceiling: what to put in a client quote

An estimate says what a request will probably cost. A client quote needs to say what it cannot exceed. Your own code can give you that number.

If your server caps the size of the user’s input and sets an output cap such as max_completion_tokens, which OpenAI’s API reference describes as “an upper bound for the number of tokens that can be generated for a completion,” then one request has a worst case:

ceiling = ( max_input_tokens × input_rate
          + max_output_tokens × output_rate ) / 1,000,000

Multiply by one plus your maximum retries and you have the ceiling per user action. Then build the quote in layers:

FigureWhere it comes fromWhat it is for
Median cost per actionYour staging logThe typical month
95th percentile per actionYour staging logThe budget
Ceiling per actionYour caps and retry limitThe worst case you can promise
Actions per user per monthThe client’s own usage estimateTheir number, not yours

That last row matters. You can measure cost per action. Only the client can estimate how many actions their users will take, so put that assumption in writing as theirs.

One trap sits inside the cap. The reasoning guide notes that if the output limit is reached while the model is still reasoning, you are still charged for the input and reasoning tokens and get no visible answer. A cap set too tight saves nothing: you pay for a failed request, then pay again for the retry. Set the cap from your logged maximums, not from a wish.

A spend limit at the provider is a useful backstop on top of all this, and so is controlling who can reach the endpoint at all. A public route anyone can call has no ceiling, however careful your arithmetic, which is why a REST endpoint with a real permission check is a cost control as much as a security one.

What this estimate will not tell you

It will not stay true. Providers change rates, retire models and change how tokens are counted, and a new model can use more tokens on the same prompt. Re-run the report after any model change and on a regular schedule, and date every figure you give a client.

It will not cover your other costs either: hosting, logging, monitoring and the developer time to maintain the feature. And it is a different question from what AI coding tools cost your team while you build, which is covered in what AI coding tools actually cost on a real project.

FAQ

How do I estimate tokens before I have any traffic? Send a realistic sample through the real API in staging and read the usage object on each response. Rules of thumb that convert characters to tokens vary with language and content type, and code often tokenizes differently from prose, so treat any conversion factor as a rough first pass.

Should I use the average cost per request? Use the median for the typical case and the 95th percentile for budgeting. An average blends the cheap and expensive requests into a number that describes neither, and it hides the long tail that drives surprises on the invoice.

Are reasoning tokens billed? On providers that expose them, yes. OpenAI’s reasoning guide says they are billed as output tokens even though they are not returned as visible text, and they appear under the reasoning token breakdown of the usage object.

Do cached tokens make my estimate lower? They can, when the start of your prompt is identical across requests and the provider applies caching. Log the cached token count separately so the report prices cached and uncached input at their own rates instead of assuming a discount.

How often should I redo the estimate? Whenever you change model, prompt or output cap, and whenever your provider changes its pricing. Date each estimate so nobody treats an old number as current.

Written by

Shah Alom

Shah Alom is the founder and writer behind Ebuhu, where he covers PHP, WordPress development, plugin and theme development, debugging, practical programming techniques, and AI-assisted coding. Drawing on hands-on web development experience, he focuses on clear, practical guidance that helps developers understand how things work, avoid common mistakes, and write more reliable code.

Leave a Reply

Your email address will not be published. Required fields are marked *