---
title: "Track OpenAI API cost per call in C#"
url: https://recursiveai.org/onellm/cookbook/track-llm-cost-per-call/
description: "Track OpenAI API cost per call in C#: OneLLM prices every call from its model catalog in decimal line items, with spend caps and JSONL logs. Free, open source."
version: "1.0.0"
---

# Track OpenAI API cost per call in C#

**OneLLM** is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneLlm) · [GitHub](https://github.com/recursive-ai-oss/onellm-csharp) · [Docs](https://recursiveai.org/onellm/docs/)

Providers bill per token, and a price per million tokens is hard to turn into the cost of one request. OneLLM prices every call as it returns, from a catalog of model prices that ships with the library.

```shell
dotnet add package RecursiveAI.OneLlm
```

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using OneLlm;

AskResult r = await Llm.GenerateAsync(
    "gpt-6-luna", "Classify: great!", []);
Console.WriteLine(r.Cost);
foreach (var line in r.Cost.Lines)
    Console.WriteLine(
        $"{line.Counter}: {line.Amount}");
```

**[Download this example (.zip)](https://recursiveai.org/onellm/cookbook/track-llm-cost-per-call/track-llm-cost-per-call.zip)**: unzip, then `dotnet run`.

Prints the cost of that one call and each of its lines. A real call to OpenAI needs `OPENAI_API_KEY`. We ran it with OneLLM’s scripted provider, which answers in the model’s place and reports a token count (here 1,200 input and 3 output tokens) for the catalog to price, so this is the scripted run’s output:

```text
$0.000122
inputTokens: 0.00012
outputTokens: 0.0000015
```

With a real model, the token counts and the cost are whatever that call used. The download runs the scripted way, with no key. `GenerateAsync` returns the answer with its usage and cost; its third argument is a list of attachments (images, PDFs), and `[]` sends none. `gpt-6-luna` and the other model names on this page are names and aliases from OneLLM’s [model catalog](https://recursiveai.org/onellm/docs/spec-catalog/), which maps each one to a provider’s model and its prices.

## The full program

This is the `Program.cs` in the download. It makes four calls to four models from three providers and prints each call’s cost line by line, then the total per model. The block between `scripted` and `end scripted` sets up stand-ins for OpenAI, Anthropic and Gemini.

```csharp
using OneLlm;
using OneLlm.Messages;
using OneLlm.Testing;

var runtime = Llm.Default;   // the runtime every Llm.* call goes through

// --- scripted: stand-ins for OpenAI, Anthropic and Gemini, so this runs without keys. ---
// They report token counts and the shipped catalog prices them as real calls. To make real
// calls, delete everything down to "end scripted" and set OPENAI_API_KEY, ANTHROPIC_API_KEY
// and GEMINI_API_KEY.
var openai = new ScriptedProvider("openai");
var anthropic = new ScriptedProvider("anthropic");
var gemini = new ScriptedProvider("gemini");
runtime.Providers.Set("openai", openai);
runtime.Providers.Set("anthropic", anthropic);
runtime.Providers.Set("gemini", gemini);
openai.Enqueue(ScriptStep.TextWithUsage("Positive.", 1_200, 3),
               ScriptStep.TextWithUsage("Three bullet points...", 8_000, 450));
anthropic.Enqueue(new ScriptStep
{
    Chunks = ["Here is the reviewed contract..."],
    Usage = new Usage { InputTokens = 2_000, CacheReadInputTokens = 30_000, OutputTokens = 900 },
});
gemini.Enqueue(ScriptStep.TextWithUsage("A short reply.", 600, 40));
// --- end scripted ---

var calls = new (string Model, string Prompt)[]
{
    ("gpt-6-luna", "Classify the sentiment of this review: ..."),
    ("gpt-6-sol", "Summarize this report in three bullets: ..."),
    ("claude-sonnet", "Review this contract against our playbook: ..."),
    ("gemini-flash", "Draft a short reply: ..."),
};

var byModel = new Dictionary<string, decimal>();
foreach (var (model, prompt) in calls)
{
    AskResult r = await Llm.GenerateAsync(model, prompt, []);
    Console.WriteLine($"{r.Usage!.Model}: {r.Usage.TotalInputTokens:N0} in, {r.Usage.OutputTokens:N0} out, {r.Cost}");
    foreach (CostLine line in r.Cost.Lines)
        Console.WriteLine($"  {line.Counter,-20} {line.Quantity,7:N0} x ${line.UnitPrice:0.00####}/M = ${line.Amount:0.000000}");
    byModel[r.Usage.Model] = byModel.GetValueOrDefault(r.Usage.Model) + r.Cost.Total;
}

Console.WriteLine($"Spent by this runtime: ${runtime.Spent:0.000000}");
foreach (var (model, total) in byModel.OrderByDescending(p => p.Value))
    Console.WriteLine($"  {model,-18} ${total:0.000000}");
```

`dotnet run` prints:

```text
gpt-6-luna: 1,200 in, 3 out, $0.000122
  inputTokens            1,200 x $0.10/M = $0.000120
  outputTokens               3 x $0.50/M = $0.000002
gpt-6-sol: 8,000 in, 450 out, $0.020500
  inputTokens            8,000 x $2.00/M = $0.016000
  outputTokens             450 x $10.00/M = $0.004500
claude-sonnet-5-5: 32,000 in, 900 out, $0.016000
  inputTokens            2,000 x $2.00/M = $0.004000
  outputTokens             900 x $10.00/M = $0.009000
  cacheReadInputTokens  30,000 x $0.10/M = $0.003000
gemini-3.8-flash: 600 in, 40 out, $0.000600
  inputTokens              600 x $0.75/M = $0.000450
  outputTokens              40 x $3.75/M = $0.000150
Spent by this runtime: $0.037222
  gpt-6-sol          $0.020500
  claude-sonnet-5-5  $0.016000
  gemini-3.8-flash   $0.000600
  gpt-6-luna         $0.000122
```

`r.Cost` is a list of `CostLine`s (counter, quantity, unit price, amount) and a `decimal` `Total`; `r.Cost` prints as dollars rounded to six places. Aliases resolve to the real model: `claude-sonnet` is billed as `claude-sonnet-5-5`, `gemini-flash` as `gemini-3.8-flash`. The Claude call shows prompt caching: 30,000 tokens read from the cache are charged at the cache-read price, not the input price. `runtime.Spent` is the running total for every call through that runtime.

The prices are the shipped catalog’s, dated 2026-11-05 and taken from each vendor’s pricing page. A model the catalog has no price for costs zero with a warning (see the article below). To track real calls, set the providers’ keys and delete the block from `// --- scripted` to `// --- end scripted ---`.

## Variations

**Refuse a call that could cost too much.** A per-call limit and a spend cap are checked before anything is sent, against the call’s worst case:

```csharp
using OneLlm;
using OneLlm.Testing;

var runtime = new Runtime { MaxCostPerCall = 0.01m, SpendCap = 5m };
runtime.Providers.Set("openai", new ScriptedProvider("openai"));
Llm.Default = runtime;

try
{
    await Llm.AskAsync("gpt-6-luna", "Summarize this report: ...");
}
catch (BudgetExceededException e)
{
    Console.WriteLine($"{e.Kind}: {e.Message}");
}
```

```text
PerCall: The call could cost up to $0.064001, above the per-call limit of $0.01.
```

**A spend cap for the whole runtime.** With `SpendCap = 1m`, a `gpt-6-sol` call is refused before it is sent, because its worst case passes the cap:

```csharp
using OneLlm;
using OneLlm.Testing;

var runtime = new Runtime { SpendCap = 1m };
runtime.Providers.Set("openai", new ScriptedProvider("openai"));
Llm.Default = runtime;

try
{
    await Llm.AskAsync("gpt-6-sol", "Summarize this report: ...");
}
catch (BudgetExceededException e)
{
    Console.WriteLine($"{e.Kind}: {e.Message}");
}
```

```text
SpendCap: Spent $0 (+$0 in flight); this call could cost up to $1.280028, past the cap of $1.
```

**Log every call’s cost to a file** and add it up later. `JsonlRecordSink` writes one JSON line per event, and `RecordReader` reads them back:

```csharp
using OneLlm;
using OneLlm.Records;
using OneLlm.Testing;

var openai = new ScriptedProvider("openai");
var runtime = new Runtime();
runtime.Providers.Set("openai", openai);
runtime.Sinks.Add(new JsonlRecordSink("records"));
Llm.Default = runtime;

openai.Enqueue(ScriptStep.TextWithUsage("Positive.", 1_200, 3), ScriptStep.TextWithUsage("Negative.", 900, 3));
await Llm.AskAsync("gpt-6-luna", "Classify: great service");
await Llm.AskAsync("gpt-6-luna", "Classify: never again");

var calls = Directory.GetFiles("records", "*.jsonl")
    .SelectMany(RecordReader.ReadFile)
    .Where(e => e.Kind == "inference")
    .OrderBy(e => e.Time)
    .Select(e => e.Json.GetProperty("usage"));
foreach (var usage in calls)
    Console.WriteLine($"{usage.GetProperty("model")}: ${usage.GetProperty("cost")}");
Console.WriteLine($"total: ${calls.Sum(u => u.GetProperty("cost").GetDecimal())}");
```

```text
gpt-6-luna: $0.0001215
gpt-6-luna: $0.0000915
total: $0.0002130
```

Without code, setting the environment variable `ONELLM_RECORDS` to a directory does the same for the default runtime.

## Related recipes

- [Unit test LLM calls in C# without an API key](https://recursiveai.org/onellm/cookbook/test-llm-code-without-api-key/): check cost logic with exact amounts.
- [Build an AI agent with tool calling in C#](https://recursiveai.org/onellm/cookbook/ai-agent-with-tools-csharp/): an agent run reports its total cost.
- [Call Ollama from C#](https://recursiveai.org/onellm/cookbook/call-ollama-from-csharp/): local models, priced at zero.
- [Get LLM structured output into a C# record](https://recursiveai.org/onellm/cookbook/structured-output-to-a-csharp-record/)

## The short answer

To track OpenAI API cost per call in C#, call the model with `Llm.GenerateAsync` from the `RecursiveAI.OneLlm` package and read `result.Cost`: a `decimal` total plus one line per usage counter (input tokens, output tokens, cache reads and writes), each with its quantity, unit price and amount. The prices come from a model catalog shipped in the package. `runtime.Spent` totals every call, `MaxCostPerCall` and `SpendCap` refuse a call before it is sent, and a JSONL record sink keeps a log you can sum later.

OneLLM is free and open source under MIT or Apache 2.0. The same works for the Anthropic and Gemini models in the catalog, and local servers such as Ollama are priced at zero. The rest of this page answers the questions people ask next.

## How much does each OpenAI API call cost?

Each call is billed by its tokens: the input tokens you send times the model’s input price, plus the output tokens it writes times the output price, with cheaper rates for input read from the prompt cache. Prices are quoted per million tokens. For `gpt-6-luna` in OneLLM’s catalog (as of 2026-11-05), that’s $0.10 per million input tokens and $0.50 per million output tokens, so the first call above cost:

- 1,200 input tokens × $0.10 / 1,000,000 = $0.000120
- 3 output tokens × $0.50 / 1,000,000 = $0.0000015
- total $0.0001215, shown as $0.000122

OneLLM’s [pricing notes](https://recursiveai.org/onellm/docs/pricing-and-counting/) give the formula for every line: `amount = quantity / per * unitPrice * multiplier`. The multiplier covers service tiers, batch calls and regions, and the notes add that “Money is `decimal` everywhere; nothing is rounded except when a cost is displayed.” For OpenAI’s own current prices, see its [pricing page](https://developers.openai.com/api/docs/pricing), which the catalog cites as its source.

## How do you calculate LLM token cost before you send?

You estimate it. OneLLM counts input tokens three ways, in order: the provider’s own count endpoint where there is one, a local tokenizer if you supply one, or an estimate by characters per token (4.0 for OpenAI-style and Gemini models, 3.5 otherwise). The notes say “The estimate errs high.”

Output can’t be known before the model writes it, so the spend checks assume the worst. From the [runtime notes](https://recursiveai.org/onellm/docs/happy-path/): the caps “are checked before a call against its worst case (local token estimate plus the full output allowance) and after against the real cost. A call that could pass a cap throws `BudgetExceededException` before anything is sent.” That’s why the per-call variation refused a short `gpt-6-luna` prompt: the worst case counts every output token the model is allowed to write, which came to $0.064001 against a $0.01 limit.

## How do you track OpenAI API usage and cost per user or tenant?

Keep the cost next to whatever you bill or limit by. Three ways, from simplest:

- **Per call.** `AskResult.Cost` and `AskResult.Usage` come back with every `GenerateAsync`. Add them to your own ledger keyed by user, tenant or feature, as the full program does by model.
- **Per runtime.** A `Runtime` keeps `Spent` and enforces its own `SpendCap`, `InstanceSpendCap` and `MaxCostPerCall`. One runtime per tenant gives each tenant its own cap.
- **As a log.** Record sinks write every inference with its usage and cost: `JsonlRecordSink` (one file per instance, in the core package), and OneLite and Entity Framework Core sinks in their own packages, which can sum a run’s cost for you (`CostAsync`, per the [stores notes](https://recursiveai.org/onellm/docs/stores/)).

Your provider’s dashboard is the bill of record. A per-call figure in your app answers the question the dashboard can’t: which request, user or feature spent it.

## What happens when a model has no price?

OneLLM doesn’t guess. The getting started guide says: “A model with no price in the catalog gives zero cost and a warning, never a guess.” The pricing notes add one exception, for servers on your own machine. A model with no price “has an unknown cost, except on a loopback server (`localhost`, `127.0.0.0/8`, `::1`), where it is free unless the catalog says otherwise.” A spend cap won’t silently pass an unknown price: the notes say the guard “refuses (rather than ignores) a limit it cannot check because the price or the output limit is unknown.”

Prices also age. The notes call a price stale 180 days past its `asOf` date: a priced call then warns when the caller supplies today’s date, and `PriceFreshness.FindStale` lists stale prices. To add a model or correct a price, add a catalog file and name it in `ONELLM_CATALOG`. `onellm catalog <model>` shows each value and which layer it came from.

Which providers have been run live: OneLLM’s [results](https://recursiveai.org/onellm/docs/results/) say a live smoke test at release passed against OpenAI (8 of 8 cases) and Gemini (7 of 7). Anthropic, Azure OpenAI / Foundry, Amazon Bedrock and Vertex AI were not run live, and are covered by recorded fixtures only.
