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.
dotnet add package RecursiveAI.OneLlmRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
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): 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:
$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, 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.
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:
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 CostLines (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:
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}"); }
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:
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}"); }
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:
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())}");
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: check cost logic with exact amounts.
- Build an AI agent with tool calling in C#: an agent run reports its total cost.
- Call Ollama from C#: local models, priced at zero.
- Get LLM structured output into a C# 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 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, 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: 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.CostandAskResult.Usagecome back with everyGenerateAsync. Add them to your own ledger keyed by user, tenant or feature, as the full program does by model. - Per runtime. A
RuntimekeepsSpentand enforces its ownSpendCap,InstanceSpendCapandMaxCostPerCall. 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).
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 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.