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.
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_KEYLiteLLM 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.
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:
fiscallm_user_id in the request's metadata or in spend_logs_metadatauser (LiteLLM's end user)So if you give each customer their own virtual key with a user_id, it works with no change to requests at all.
| Value | From |
|---|---|
| Cost | response_cost |
| Model and provider | model, custom_llm_provider |
| Tokens | prompt_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.
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.
Use the FiscaLLM Python SDK after the call (meter.track(...) with the cost from litellm.completion_cost), or send traces with OpenTelemetry.
No. The generic_api callback is part of the open-source proxy. This setup was tested on the open-source LiteLLM 1.83.9.
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.
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.
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.
They are skipped and counted in the response under skipped.noUser, with a hint. Cost without a user cannot be matched to revenue.