Usage Metering and Billing
Every API response reports what the request fed to each model family and what it produced. A platform that resells AI work (credits per tenant, a price per page) reads this report and applies its own prices. The server measures, and your application bills.
Two principles drive the numbers:
- Only what reached a model counts. A 100-page PDF sent to classification that reads three pages reports three. A page read from its text layer without a model reports nothing.
- A page counts once per model family. The document parser can read a page twice (a second reading at a higher resolution, a crop of a table or chart). Each family still counts that page once, so internal quality passes never inflate a bill.
1Where the report is#
| Response | Carrier |
|---|---|
JSON body of a /lmkit/v1 endpoint |
a usage field at the root of the body |
| Any metered response (binary files, OpenAI-, Anthropic- and Ollama-compatible endpoints) | the X-LMKit-Usage header, the same object as compact JSON |
| Streamed chat, chat with documents, grounded answers | the terminal event (done event or the frame with "done": true) |
OpenAI-compatible chat stream with stream_options.include_usage |
lmkit_usage on the usage-bearing final chunk |
| Background job | usage at the root of GET /lmkit/v1/jobs/{job_id} once the job has ended, and the X-LMKit-Usage header on that poll |
A stream starts before its work is done, so it never carries the header: read its final event. A 202 Accepted carries no usage: the work continues in the job, and the job reports it. A
failed or cancelled request reports the work it did before it stopped, with partial: true, in
the header (and on the job). Job polling and the admin API are not metered.
The header is listed in Access-Control-Expose-Headers, so a browser client can read it.
2The object#
"usage": {
"operation": "document-parsing",
"input": {
"llm": { "characters": 0, "tokens": 0 },
"vlm": { "images": 2, "tokens": 834 },
"ocr": { "images": 0 },
"layout": { "images": 2 },
"image": { "images": 0 },
"embedding": { "characters": 0, "images": 0, "items": 0 },
"speech": { "audio_seconds_analyzed": 0, "audio_seconds_transcribed": 0 },
"training": { "tokens": 0, "steps": 0, "duration_ms": 0 },
"tools": { "calls": {} }
},
"output": { "tokens": 92, "characters": 180, "items": 0 },
"partial": false,
"duration_ms": 12893,
"models": [
{ "model": "lmkit-doc-parser:1", "kind": "vlm", "calls": 2, "input_tokens": 834, "output_tokens": 92, "duration_ms": 755 },
{ "model": "docling-layout-heron", "kind": "layout", "calls": 2, "input_tokens": 0, "output_tokens": 0, "duration_ms": 210 }
]
}
Every block is always present, with zeros when a family did no work, so one parser reads every endpoint.
Billable units
| Field | Counts |
|---|---|
input.llm.characters |
Content placed into prompts: the new messages of the request, the input text of a task endpoint (translation, extraction, classification and the rest), and the text of document pages read as text, each page once. The instructions the server adds are not counted. A chat client that sends its whole history again is counted for its new messages only. A task that reads the same input in several passes counts it once. |
input.vlm.images |
Distinct pages and images read by a vision-language model. |
input.ocr.images |
Distinct pages and images read by an OCR engine. |
input.layout.images |
Distinct pages analyzed by the layout detection model. |
input.image.images |
Images processed by a non-generative image model (background removal). |
input.embedding.characters / images / items |
Text embedded or reranked, images embedded, and vectors computed or documents scored. Cached vectors count too, so a figure never depends on what earlier requests left in a cache. |
input.speech.audio_seconds_analyzed |
Audio scanned by voice activity detection. |
input.speech.audio_seconds_transcribed |
Speech transcribed; silence the detector skipped is not. |
input.training.tokens / steps / duration_ms |
Dataset tokens trained on times the epochs run, optimizer steps, and wall time. A run stopped early counts the share it trained. |
input.tools.calls |
Tool calls by tool name, from chat and agents. |
output.characters |
Generated text, transcripts included. |
output.items |
Discrete results returned: entities, PII entities, keywords, extracted fields, segments, categories. |
One page can count in several families: a scan read by the layout model, then OCR, then the
reader counts once in layout, once in ocr and once in vlm. Each family has its own price,
which can be zero.
Cost figures
tokens in input.llm and input.vlm, output.tokens and the models list describe what
the work cost you: instructions, repeated passes and internal re-reads included. Use them for
margins and capacity planning, not for a tenant's bill. models[].calls shows the re-reads
that the billable counts leave out.
3Reserving credit before the work runs#
POST /lmkit/v1/file-info returns usage_estimate: the most a file can feed to models, in the
same units.
"usage_estimate": { "images": 5, "characters": 0, "audio_seconds": 0 }
| File | Estimate |
|---|---|
| PDF, image (each frame of a TIFF) | images: pages or frames |
| Word, HTML, email, text and other reflowable documents | characters: the document's text |
| Audio and video | audio_seconds: the duration of the track |
A typical flow: estimate, convert the estimate to your own unit, reserve that much of the
tenant's credit, run the request, then settle on the usage it returns. A request that reads
only part of the file (a page range, a classification that reads the first pages) settles
below its reservation.
4Turning usage into a price#
The server never applies a price. One way to bill per page, with a page at a fixed price and reflowable content converted to page equivalents:
pages = input.vlm.images
+ input.ocr.images (if OCR is billed on its own)
+ ceil(input.llm.characters / 3000)
+ ceil(input.speech.audio_seconds_transcribed / 60)
Store the whole usage object with each billed request: operation tells you which endpoint
did the work, and models explains any figure a tenant questions.
5Using it from the SDK#
The server reads these figures from LM-Kit.NET's UsageScope. An application that embeds the
SDK directly meters the same way:
using LMKit.Telemetry;
using (var usage = UsageScope.Begin())
{
var parsed = await parser.ParseAsync("contract.pdf");
UsageReport report = usage.GetReport();
Console.WriteLine($"{report.Vlm.Images} pages read by the reader");
}
On a farm. Usage is measured on the node that runs the work and travels with the response or the job record, so any node answers a job poll with the job's usage.
See also Errors, Retries, and Jobs for the job contract, Building a Multi-Tenant SaaS for per-tenant keys, and The Cost Math for what the work costs to run.