AI workloads¶
Every tool in this section ships on every plan. Cost tracking for AI is commodity; what LumaTrack adds is the other column: the value those runs produced, in the same auditable ledger as the rest of your automation.
Which path should I use?¶
| You have | Use | Why |
|---|---|---|
| n8n, Make, a webhook, a script, or any HTTP client | POST /api/v1/runs with an ai object |
One call per execution; the exact schema is below. |
| An app instrumented with OpenTelemetry GenAI (LangChain, LlamaIndex, vendor SDKs) | the OTLP endpoint | No code changes: point the exporter at LumaTrack. |
| A Python stack routing calls through LiteLLM | the LiteLLM callback | Two lines of config. |
| An AI agent that should file its own evidence | the MCP server | The agent records runs and queries value itself. |
All four paths land in the same place: a run event on the automation's ledger, priced when the model is known (or an override or client cost is given), stamped with the price-table version in force.
First priced run in ten minutes¶
- Create an automation (Automations, New automation) and note its slug, or
pick one from
GET /api/v1/automations. - Mint an ingest-scoped API key (Settings, API keys).
- Send one event (n8n: paste this into an HTTP Request node set to POST/JSON):
curl -X POST "$LUMATRACK_URL/api/v1/runs" \
-H "Authorization: Bearer $LUMATRACK_KEY" \
-H "Content-Type: application/json" \
-d '{"automation": "<your-slug>", "status": "success",
"ai": {"model": "claude-sonnet-5", "input_tokens": 10000, "output_tokens": 2000}}'
- The response echoes the priced result (
ai.cost,ai.price_version). Open the run under Runs to see the same figures, and Analytics picks the spend up in its coverage line.
The usage object is named ai, not ai_usage or usage (that last one is
the per-unit metering map). Misspelled top-level fields, and misspelled keys
inside the ai object, come back in the response under ignored_fields
(keys inside metadata and usage are yours and are not checked). Treat a
non-empty ignored_fields as an integration bug.
Token-priced run events¶
Add an ai object to any run event and LumaTrack prices
it server-side from a maintained table of 2,400+ models (sourced from the
open LiteLLM dataset, refreshed weekly), as Decimal, stamped with the
price-table version in force so the figure stays auditable when prices move:
curl -X POST "$LUMATRACK_URL/api/v1/runs" \
-H "Authorization: Bearer $LUMATRACK_KEY" \
-H "Content-Type: application/json" \
-d '{
"automation": "phishing-triage-agent",
"status": "success",
"ai": {
"model": "claude-sonnet-5",
"input_tokens": 10000,
"output_tokens": 2000,
"cached_tokens": 50000
}
}'
input_tokens are non-cached input tokens; cached_tokens are cache reads,
billed at the model's cache rate (or the input rate when the model has none).
Send "cost" to override the computation with your own figure (stamped
client). An unknown model is stored unpriced and flagged in the response;
LumaTrack never guesses a price. Set your negotiated rates per model under
your organization's price overrides.
The computed cost lands in the ledger's per-run cost bucket, next to labor value, so an AI automation's net is real: value minus what the tokens cost.
Flat-rate subscriptions (billing basis)¶
Agents that run on a flat-rate plan (Claude Max seats, Copilot) cost you nothing per call; pricing their tokens at API list would book a number you never paid. Mark the usage instead:
"ai": {"model": "claude-opus-4-8", "input_tokens": 120000,
"output_tokens": 50000, "billing": "subscription"}
LumaTrack then books $0 marginal cost to the ledger and records the
API-equivalent as list_cost on the run: the repricing exposure, kept
with the same price-version audit stamp. Model the plan fee itself as a
recurring cost component on the automation, which is where a subscription
honestly belongs. Set a per-automation default under the automation's
settings (ai_billing) so flag-less flows (OTLP spans, n8n) inherit it;
an explicit billing on the event always wins. On the LiteLLM proxy, use
lumatrack_billing: subscription in request metadata or
LUMATRACK_AI_BILLING=subscription for the whole proxy.
The counterpart is "billing": "metered": API-billed traffic, priced from
the model table and booked as real spend. It is the default, so you only
send it explicitly to override an automation whose ai_billing default is
subscription. The common case is a mostly-flat-rate agent whose overflow
calls fall through to the API and should book their actual cost.
"ai": {"model": "claude-opus-4-8", "input_tokens": 120000,
"output_tokens": 50000, "billing": "metered"}
Why this matters: flat-rate AI pricing is being repriced across the industry (GitHub Copilot moved every plan to usage-based billing on 2026-06-01; Anthropic capped and then re-worked subscription agent usage through 2025 to 2026). The scenarios page prices exactly that risk.
What-if scenarios (Team and above)¶
The Scenarios page re-evaluates your as-reported ledger under conditions you choose (an AI repricing, an assumption haircut, a volume drop) without changing a booked number, and shows which automations survive each. The flat-rate toggle prices your subscription-covered usage at list, so you can see your exposure if a provider moves you to usage-based billing.
Full walkthrough: What-if scenarios.
MCP server¶
LumaTrack ships a stateless MCP endpoint at POST /mcp (Streamable HTTP,
JSON responses, no sessions), authenticated with your existing API key:
{
"mcpServers": {
"lumatrack": {
"url": "https://lumatrack.io/mcp",
"headers": {"Authorization": "Bearer lmt_..."}
}
}
}
The server covers the main actions of the site, so an agent can run the whole value loop, not just file evidence:
- Evidence:
record_run(tokens included; the exact ingestion the REST API uses),record_event/resolve_event(the incidents loss ledger),list_automations,list_event_types. - Reads:
get_roi_summary,get_automation_roi,get_automation,list_runs,list_events,list_workspaces,list_roles,list_periods,get_analytics_series(the in-app chart engine),query_ledger(the auditable drill-down), and the modeling lists (cost components, baseline/oversight steps, value streams, projection versions). - Lifecycle and modeling:
create_automation,update_automation,promote_automation(measuring to active),create_cost_component,create_baseline_step,create_oversight_step,create_value_stream,create_workspace,create_role,create_event_type. - Initiatives (Team+):
list_initiatives,create_initiative,implement_initiative,transition_initiative; shared costs (Business+):list_shared_costs,create_shared_cost; what-if:get_scenario_stress(Team+); variance/NPV:get_forecast_variance(Business+). - Sharing and integration:
list_report_links,create_report_link,revoke_report_link(the kill switch, and the only destructive tool),list_webhooks(secret redacted; it is served once, at creation),create_webhook.
Deliberately absent: account, billing, member and API-key management, month close, deletions, and MSP provisioning. Plan gates mirror the UI exactly. An ingest-scope key sees only the ingest-shaped tools (recording, resolving, and the two slug listings), mirroring the REST scope rules, so an agent can file its own evidence with a key that cannot read your ledger.
Like the REST endpoint, record_run echoes an ignored_fields list when a
payload carries keys the ingester does not know. Treat a non-empty
ignored_fields as a bug in your tool call; a misspelled input_tokens
must never be a silent success.
OpenTelemetry ingest¶
If your stack already emits OpenTelemetry GenAI spans, you do not need our SDK.
Point an OTLP/HTTP exporter at POST /otel/v1/traces (JSON encoding) with your
API key as a bearer token. Spans carrying gen_ai.* attributes become priced
run events; traceId:spanId is the idempotency key, and an OTLP error status
maps to a failed run. Unmatched spans are rejected and counted in the response,
never guessed.
Point the exporter at LumaTrack (any SDK whose OTLP exporter speaks the
http/json protocol, such as JS/Node):
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://lumatrack.io/otel/v1/traces"
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer lmt_..."
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/json"
Not every language can: the Python OTLP exporter, for one, only speaks grpc
and http/protobuf. Stacks like that (and anything already routing through an
OpenTelemetry Collector) bridge with the collector's otlphttp exporter,
which re-encodes to JSON:
exporters:
otlp_http/lumatrack: # named otlphttp in collectors older than ~v0.156
traces_endpoint: "https://lumatrack.io/otel/v1/traces"
encoding: json
headers:
Authorization: "Bearer lmt_..."
service:
pipelines:
traces:
receivers: [otlp] # keep your existing receivers and processors
exporters: [otlp_http/lumatrack]
An exporter that is not listed in a service.pipelines.traces entry sends
nothing; the service stanza above is what turns it on. The collector
compresses bodies with gzip by default; the endpoint accepts that as-is
(no compression setting needed), with the decompressed size bounded by
the same 2.5MB cap as an uncompressed request.
Python stacks without a collector can skip OTel entirely: the LiteLLM callback and the REST API carry the same data.
Route each span to an automation in one of two ways: set the
lumatrack.automation span attribute to the automation's slug (per call), or
name your service.name resource attribute after the slug (per service). The
automation must already exist. The span's billing basis follows the
automation's ai_billing default (see above); mark flat-rate services
subscription on the automation.
Both /mcp and /otel/v1/traces share your organization's
API rate limit with the REST API; a 429
with a Retry-After header means back off that many seconds. OTel SDK
exporters batch spans (many spans per request) and retry on 429 on their
own, so at the plans' limits this is not something a normal setup hits.
What LumaTrack reads (OTel GenAI semantic conventions, so we never invent a
convention that drifts): gen_ai.request.model (or gen_ai.response.model)
for the model; gen_ai.usage.input_tokens and gen_ai.usage.output_tokens
for tokens; gen_ai.usage.cache_read_input_tokens (or gen_ai.usage.cached_tokens)
for cache reads.
Supported AI vendors and instrumentations¶
Any library that emits the gen_ai.* conventions works. Python-based
instrumentations (OpenLLMetry, OpenInference) reach the endpoint through the
collector bridge above. Verified paths:
- OpenAI and Anthropic via OpenLLMetry (Traceloop):
pip install traceloop-sdk, thenTraceloop.init(app_name="password-reset-agent"): the app name becomesservice.name, so name it after the automation's slug. It auto-instruments the OpenAI and Anthropic Python SDKs and emitsgen_ai.usage.*per call. - OpenAI and Anthropic via OpenInference (Arize/Phoenix): the
openinference-instrumentation-openai/-anthropicpackages emit the same attributes; add alumatrack.automationspan attribute via a span processor. - LangChain / LlamaIndex: both OpenLLMetry and OpenInference ship framework instrumentors; the underlying OpenAI/Anthropic calls carry the token attributes LumaTrack prices.
- Claude Code fleets (trace export is beta upstream): enable it with
CLAUDE_CODE_ENABLE_TELEMETRY=1,CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1andOTEL_TRACES_EXPORTER=otlp, then set the threeOTEL_EXPORTER_OTLP_TRACES_*variables above (Claude Code speakshttp/jsonnatively). Itsclaude_code.llm_requestspans carrygen_ai.request.modelwith token counts as bareinput_tokens/output_tokens/cache_read_tokensattributes; LumaTrack reads those alongside thegen_ai.usage.*names. Route them by creating an automation whose slug matches the emittedservice.name(the rejection message inpartialSuccesstells you the exact name it looked for). - Bedrock / Vertex-hosted OpenAI or Anthropic models: instrument at the SDK
layer (OpenLLMetry/OpenInference) so the
gen_ai.request.modelreflects the model you want priced. Unknown model ids are stored unpriced and flagged; add a per-org price override for a private model id.
Tokens are the source of truth; if your instrumentation also emits a cost
attribute you trust, send it through the REST ai.cost override instead.
LiteLLM callback¶
Running the LiteLLM proxy? One callback turns every proxied LLM call into a run event, with LiteLLM's own cost figure attached:
Download litellm_lumatrack.py next to your
proxy config (or anywhere on the proxy's PYTHONPATH), then:
litellm_settings:
callbacks: ["litellm_lumatrack.LumaTrackLogger"]
Set LUMATRACK_URL and LUMATRACK_KEY in the proxy environment and put
lumatrack_automation: <slug> in request metadata (per key, team, or call).
Calls without a mapping are skipped.
Billed spend and coverage¶
Token math estimates drift 1 to 3% from the invoice. Paste an OpenAI or Anthropic admin key under Settings and LumaTrack imports your billed daily AI spend (keys are encrypted at rest and never displayed again). The analytics page then shows the honest FinOps number: how much of your billed AI spend is attributed to tracked automations, and how much nothing is measuring yet.
Troubleshooting¶
- 401: the key is wrong, revoked, or missing the
Bearerprefix. - 404 "No automation with slug ...": create the automation first, or list
slugs with
GET /api/v1/automations. OTel spans resolve the automation fromlumatrack.automationor the resource'sservice.name. ignored_fieldsin the response: a top-level field, or a field inside theaiobject, was misspelled and skipped. The run is stored; the listed fields are not.ai.pricedfalse / zero cost: the model id is not in the price table and no override exists. Add one under Settings, Model price overrides, or send the cost you were billed inai.cost.- 429: you hit your plan's per-minute rate limit; honor
Retry-After.