Integrations

LiteLLM cost per user, next to revenue

In short: add a generic_api callback to the LiteLLM proxy config that posts to https://fiscallm.com/api/v1/litellm, and send user with each request. Every successful call becomes a cost event for that user, priced with LiteLLM's own figure. Nothing changes in your application code.

1. Add the callback

Create an app, copy the ingest key, and set it as FISCALLM_KEY in the proxy's environment. Then in config.yaml:

litellm_settings:
  callbacks: ["fiscallm"]

callback_settings:
  fiscallm:
    callback_type: generic_api
    endpoint: https://fiscallm.com/api/v1/litellm
    headers:
      Authorization: os.environ/FISCALLM_KEY

LiteLLM fills in os.environ/FISCALLM_KEY from the environment, so the key never sits in the file. Restart the proxy. It keeps answering requests exactly as before; logs are posted in the background after each one.

2. Send the user with each request

Use the ID your billing knows (the RevenueCat app user ID, or the user_id in Stripe metadata). The standard OpenAI user field is enough:

const res = await openai.chat.completions.create({
  model: "gpt-5-mini",
  messages,
  user: userId,                              // who the cost belongs to
  metadata: { tags: ["feature:search"] },    // optional: split cost by feature
});

The user is taken from the first of these that is set:

  1. fiscallm_user_id in the request's metadata or in spend_logs_metadata
  2. the request's user (LiteLLM's end user)
  3. the end user or internal user attached to the virtual key

So if you give each customer their own virtual key with a user_id, it works with no change to requests at all.

What is read from a log

ValueFrom
Costresponse_cost
Model and providermodel, custom_llm_provider
Tokensprompt_tokens, completion_tokens, and cache reads and writes from the usage object
Feature (optional)a feature:<name> request tag, or feature in the metadata
Job (optional)job_id in the metadata, for pay-per-job apps

Failed requests are skipped because they cost nothing. Each log is stored once, so a retried callback cannot double count.

3. Check that it works

The app's Setup page shows the first event when it arrives. You can also post one log by hand; the answer says what was stored and what was skipped:

curl https://fiscallm.com/api/v1/litellm \
  -H "Authorization: Bearer $FISCALLM_KEY" -H "Content-Type: application/json" \
  -d '[{"id":"test-1","status":"success","model":"gpt-4o-mini","custom_llm_provider":"openai",
        "end_user":"user_123","prompt_tokens":1200,"completion_tokens":300,
        "response_cost":0.00036,"endTime":'$(date +%s)'}]'

{"accepted":1,"duplicates":0,"skipped":{"failedRequests":0,"noUser":0,"badTime":0}}

Then connect revenue (RevenueCat, Stripe or a CSV) and the dashboard shows margin per user. The same numbers are available to scripts and AI agents through the MCP server and the read API.

Using the LiteLLM Python SDK without the proxy

Use the FiscaLLM Python SDK after the call (meter.track(...) with the cost from litellm.completion_cost), or send traces with OpenTelemetry.

Questions

Does this need LiteLLM Enterprise?

No. The generic_api callback is part of the open-source proxy. This setup was tested on the open-source LiteLLM 1.83.9.

LiteLLM already tracks spend per end user. What does FiscaLLM add?

Revenue. LiteLLM shows what a user cost. FiscaLLM puts that next to what the same user paid through RevenueCat, Stripe or your billing, net of fees, so you see margin per user, plan and trial, and get alerted when a paying user loses money.

Which cost is used, LiteLLM's or FiscaLLM's?

LiteLLM's response_cost, as is. So custom prices, discounts and models you priced yourself in LiteLLM carry over. If a log has no cost, FiscaLLM prices it from the token counts.

Are prompts and responses sent?

LiteLLM's log can include them, depending on your settings. FiscaLLM never reads or stores them. To keep them off the wire entirely, set turn_off_message_logging: true under litellm_settings.

What happens to requests with no user?

They are skipped and counted in the response under skipped.noUser, with a hint. Cost without a user cannot be matched to revenue.