Ingest LiteLLM spend logs
const url = 'http://localhost:8080/v1/integrations/litellm/spend';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"id":"example","status":"example","cache_hit":true,"call_type":"example","model":"example","custom_llm_provider":"example","user":"example","end_user":"example","usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":1,"prompt_tokens_details":{"cached_tokens":1,"cache_creation_tokens":1,"cache_creation_token_details":{"ephemeral_5m_input_tokens":1,"ephemeral_1h_input_tokens":1}},"cache_read_input_tokens":1,"cache_creation_input_tokens":1},"prompt_tokens":1,"completion_tokens":1,"total_tokens":1,"response_cost":1,"cost_breakdown":{"input_cost":1,"output_cost":1,"cache_read_cost":1,"cache_creation_cost":1,"tool_usage_cost":1,"total_cost":1},"startTime":1,"endTime":1,"metadata":{}}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url http://localhost:8080/v1/integrations/litellm/spend \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "id": "example", "status": "example", "cache_hit": true, "call_type": "example", "model": "example", "custom_llm_provider": "example", "user": "example", "end_user": "example", "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "prompt_tokens_details": { "cached_tokens": 1, "cache_creation_tokens": 1, "cache_creation_token_details": { "ephemeral_5m_input_tokens": 1, "ephemeral_1h_input_tokens": 1 } }, "cache_read_input_tokens": 1, "cache_creation_input_tokens": 1 }, "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "response_cost": 1, "cost_breakdown": { "input_cost": 1, "output_cost": 1, "cache_read_cost": 1, "cache_creation_cost": 1, "tool_usage_cost": 1, "total_cost": 1 }, "startTime": 1, "endTime": 1, "metadata": {} }'Target for LiteLLM Proxy’s generic_api logging callback. Accepts
one LiteLLM log record, a JSON array of records, or
{"events": [...]}. Each completion, chat, text-completion or
embedding record becomes up to five usage events on the tokens
meter, one per token_type dimension: input (prompt tokens not
read from or written to the cache), cache_read, cache_write_5m,
cache_write_1h and output. A record’s events are recorded in
one transaction. Each event also carries model (a model family
name when recognised), model_raw and provider, plus team_id
and request_tags from metadata when present. Other call types,
records with no token counts, failed calls and LiteLLM
response-cache hits are counted as skipped.
The customer is the record’s user or end_user (or
metadata.user_api_key_end_user_id), matched against the
customer’s external_id. Event idempotency keys are the record
id plus the token type, so a resent record is counted as
deduplicated, not billed twice.
A token row whose customer or meter does not exist yet is held,
not billed and not lost: it is counted in held and listed by
GET /v1/usage-events/held until you release or discard it. A
resent row that is already held is counted as deduplicated. A
record with no user or no id, or an invalid event, is listed in
errors and the rest of the batch still goes in, with status 200.
If the usage store cannot be reached, the whole request fails with
503 so LiteLLM retries it. Requires usage write permission.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”The fields of a LiteLLM StandardLoggingPayload that Hardcount reads.
Other fields are accepted and ignored.
object
LiteLLM’s call id. Used to build the events’ idempotency keys.
A failure record (a call the provider refused or that timed out) is skipped, whatever token counts it carries.
true when LiteLLM answered from its own response cache. The
record is skipped and counted in cache_hits: the provider
charged nothing.
A record bills on its token counts unless its call type is one Hardcount does not bill (images, audio, realtime, moderation, batches, files, A2A, response retrieval, …); those are skipped, and a skipped record that carries tokens is logged.
The customer’s external_id.
Where LiteLLM Proxy puts the caller’s user. Used when user is empty.
object
Includes cache-read tokens.
object
Anthropic’s cache writes by cache lifetime. They become
cache_write_5m and cache_write_1h events, taken out
of input. Without this split, or when it does not add
up to the cache-write count, the writes stay in input.
object
Cache-write tokens, included in prompt_tokens. Split into cache_write_5m and cache_write_1h by prompt_tokens_details.cache_creation_token_details.
Used when there is no usage object.
Accepted but not stored.
The provider’s own cost, in dollars. When present, each event’s
provider cost comes from its part (input, output, cache read or
cache write) instead of the provider-costs table. A part that is
missing or negative falls back to the table. input_cost
includes cache reads and cache writes, so the input event gets
input_cost minus cache_read_cost and cache_creation_cost.
If there were cache reads but no cache_read_cost, or split-off
cache writes but no cache_creation_cost, the input-side events
use the provider-costs table. With 1-hour cache writes they
always do: LiteLLM prices cache_creation_cost at the 5-minute
rate.
object
Unix seconds. Event time when endTime is absent.
Unix seconds. The event time. With neither, the customer’s current time is used.
team_id and request_tags become event dimensions;
usage_object is read for token counts; other keys are ignored.
object
The fields of a LiteLLM StandardLoggingPayload that Hardcount reads.
Other fields are accepted and ignored.
object
LiteLLM’s call id. Used to build the events’ idempotency keys.
A failure record (a call the provider refused or that timed out) is skipped, whatever token counts it carries.
true when LiteLLM answered from its own response cache. The
record is skipped and counted in cache_hits: the provider
charged nothing.
A record bills on its token counts unless its call type is one Hardcount does not bill (images, audio, realtime, moderation, batches, files, A2A, response retrieval, …); those are skipped, and a skipped record that carries tokens is logged.
The customer’s external_id.
Where LiteLLM Proxy puts the caller’s user. Used when user is empty.
object
Includes cache-read tokens.
object
Anthropic’s cache writes by cache lifetime. They become
cache_write_5m and cache_write_1h events, taken out
of input. Without this split, or when it does not add
up to the cache-write count, the writes stay in input.
object
Cache-write tokens, included in prompt_tokens. Split into cache_write_5m and cache_write_1h by prompt_tokens_details.cache_creation_token_details.
Used when there is no usage object.
Accepted but not stored.
The provider’s own cost, in dollars. When present, each event’s
provider cost comes from its part (input, output, cache read or
cache write) instead of the provider-costs table. A part that is
missing or negative falls back to the table. input_cost
includes cache reads and cache writes, so the input event gets
input_cost minus cache_read_cost and cache_creation_cost.
If there were cache reads but no cache_read_cost, or split-off
cache writes but no cache_creation_cost, the input-side events
use the provider-costs table. With 1-hour cache writes they
always do: LiteLLM prices cache_creation_cost at the 5-minute
rate.
object
Unix seconds. Event time when endTime is absent.
Unix seconds. The event time. With neither, the customer’s current time is used.
team_id and request_tags become event dimensions;
usage_object is read for token counts; other keys are ignored.
object
object
The fields of a LiteLLM StandardLoggingPayload that Hardcount reads.
Other fields are accepted and ignored.
object
LiteLLM’s call id. Used to build the events’ idempotency keys.
A failure record (a call the provider refused or that timed out) is skipped, whatever token counts it carries.
true when LiteLLM answered from its own response cache. The
record is skipped and counted in cache_hits: the provider
charged nothing.
A record bills on its token counts unless its call type is one Hardcount does not bill (images, audio, realtime, moderation, batches, files, A2A, response retrieval, …); those are skipped, and a skipped record that carries tokens is logged.
The customer’s external_id.
Where LiteLLM Proxy puts the caller’s user. Used when user is empty.
object
Includes cache-read tokens.
object
Anthropic’s cache writes by cache lifetime. They become
cache_write_5m and cache_write_1h events, taken out
of input. Without this split, or when it does not add
up to the cache-write count, the writes stay in input.
object
Cache-write tokens, included in prompt_tokens. Split into cache_write_5m and cache_write_1h by prompt_tokens_details.cache_creation_token_details.
Used when there is no usage object.
Accepted but not stored.
The provider’s own cost, in dollars. When present, each event’s
provider cost comes from its part (input, output, cache read or
cache write) instead of the provider-costs table. A part that is
missing or negative falls back to the table. input_cost
includes cache reads and cache writes, so the input event gets
input_cost minus cache_read_cost and cache_creation_cost.
If there were cache reads but no cache_read_cost, or split-off
cache writes but no cache_creation_cost, the input-side events
use the provider-costs table. With 1-hour cache writes they
always do: LiteLLM prices cache_creation_cost at the 5-minute
rate.
object
Unix seconds. Event time when endTime is absent.
Unix seconds. The event time. With neither, the customer’s current time is used.
team_id and request_tags become event dimensions;
usage_object is read for token counts; other keys are ignored.
object
Example generated
{ "id": "example", "status": "example", "cache_hit": true, "call_type": "example", "model": "example", "custom_llm_provider": "example", "user": "example", "end_user": "example", "usage": { "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "prompt_tokens_details": { "cached_tokens": 1, "cache_creation_tokens": 1, "cache_creation_token_details": { "ephemeral_5m_input_tokens": 1, "ephemeral_1h_input_tokens": 1 } }, "cache_read_input_tokens": 1, "cache_creation_input_tokens": 1 }, "prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 1, "response_cost": 1, "cost_breakdown": { "input_cost": 1, "output_cost": 1, "cache_read_cost": 1, "cache_creation_cost": 1, "tool_usage_cost": 1, "total_cost": 1 }, "startTime": 1, "endTime": 1, "metadata": {}}Responses
Section titled “Responses”The batch was processed. Check errors for records that were not recorded.
object
Usage events newly recorded.
Usage events already recorded, or already held, by an earlier delivery.
Usage events kept because their customer or meter does not
exist yet. Not billed until released with
POST /v1/usage-events/held/release.
Records that bill nothing - a call type with no tokens, zero tokens, a failed call (status failure) or a LiteLLM response-cache hit.
The LiteLLM response-cache hits among skipped. LiteLLM’s own spend counts them as $0.
Of accepted, the events answered billing_status: after_close: their time was already invoiced. A replay never
counts here.
Of accepted, the events no price of the subscription’s plan
takes (billing_status: no_price), such as a model no price
names. A replay never counts here.
Of accepted, the events whose meter the subscription’s plan
does not price (billing_status: not_on_plan). A replay never
counts here.
Of accepted, the events whose time no subscription of the
customer bills (billing_status: outside_subscription). A replay
never counts here.
One entry per record that was not recorded. A record’s events are recorded together or not at all.
object
The LiteLLM record id.
Example generated
{ "accepted": 1, "deduplicated": 1, "held": 1, "skipped": 1, "cache_hits": 1, "after_close": 1, "no_price": 1, "not_on_plan": 1, "outside_subscription": 1, "errors": [ { "id": "example", "error": "example" } ]}invalid_request: empty body or not valid JSON.
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}ingest_unavailable: the usage store could not be reached.
Retry the whole batch; events already recorded come back as
deduplicated.
object
object
One of invalid_request_error, authentication_error,
rate_limit_error or api_error. Every error the API
answers has this shape, including an unknown path (404,
code route_not_found) and a method the path does not take
(405, code method_not_allowed, with an Allow header).
The few exceptions are listed in docs/api-surface.md.
The request field the error is about. Sent only when the error
names one. A request body may hold only the fields its
operation lists, spelled and cased as listed: any other
field, or one the server sets (such as id), is refused
with 422 and param naming it. An operation with no request body lists no fields: send
no body, {} or null. The Stripe and Postmark webhooks and
the LiteLLM callback take their sender’s body as it comes.
Example
{ "error": { "type": "invalid_request_error" }}