Add nullable tokens_in / tokens_outIntegerFields to PromptMetric to record real prompt/completion token counts per turn.
New extract_token_usage() helper parses provider usage payloads (LangChain usage_metadata, OpenAI-style prompt_tokens/completion_tokens, Ollama prompt_eval_count/eval_count). When a provider reports no usage, the fields stay null — counts are never estimated/fabricated.
create_prompt_metric / finish_prompt_metric in both consumers.py and consumers_graph.py accept and persist optional tokens_in / tokens_out (added to update_fields only when present).
The streaming chat path uses LangChain StrOutputParser, which yields plain string chunks with no usage metadata, so live turns currently persist null tokens (honest, per acceptance criteria — no fabricated counts). The plumbing + helper are in place so wiring real provider usage is a drop-in once the services expose it.
Follow-ups
#16 — Show token in/out in chat web app UI (FE + API exposure)
#17 — Token-based billing, quotas, and enforcement
Test plan
uv run python manage.py test — full suite green (266 tests, 6 skipped)
Model: token fields default null + persist when set
Metric lifecycle: tokens persist when provided, stay null when absent (both consumers)
Admin: conversation token totals sum across metrics and ignore other conversations
Closes #15
## Summary
- Add nullable `tokens_in` / `tokens_out` `IntegerField`s to `PromptMetric` to record real prompt/completion token counts per turn.
- New `extract_token_usage()` helper parses provider usage payloads (LangChain `usage_metadata`, OpenAI-style `prompt_tokens`/`completion_tokens`, Ollama `prompt_eval_count`/`eval_count`). When a provider reports no usage, the fields stay **null** — counts are never estimated/fabricated.
- `create_prompt_metric` / `finish_prompt_metric` in both `consumers.py` and `consumers_graph.py` accept and persist optional `tokens_in` / `tokens_out` (added to `update_fields` only when present).
- Admin panel (this ticket's deliverable):
- `PromptMetricAdmin` lists `tokens_in` / `tokens_out` and adds `event` / `model_name` / `has_file` filters.
- `ConversationAdmin` shows summed `tokens_in` / `tokens_out` / `tokens_total` per conversation.
- Migration `0023_promptmetric_tokens_in_promptmetric_tokens_out` (existing rows remain valid — null).
## Note on live capture
The streaming chat path uses LangChain `StrOutputParser`, which yields plain string chunks with no usage metadata, so live turns currently persist `null` tokens (honest, per acceptance criteria — no fabricated counts). The plumbing + helper are in place so wiring real provider usage is a drop-in once the services expose it.
## Follow-ups
- #16 — Show token in/out in chat web app UI (FE + API exposure)
- #17 — Token-based billing, quotas, and enforcement
## Test plan
- [x] `uv run python manage.py test` — full suite green (266 tests, 6 skipped)
- [x] Model: token fields default null + persist when set
- [x] `extract_token_usage`: LangChain / OpenAI / Ollama key variants, attribute sources, bool/float handling, missing usage → (None, None)
- [x] Metric lifecycle: tokens persist when provided, stay null when absent (both consumers)
- [x] Admin: conversation token totals sum across metrics and ignore other conversations
Closes#15
Add nullable tokens_in / tokens_out IntegerFields to PromptMetric so we
record real prompt/completion token counts per turn. Values come from the
LLM provider usage payload via a new extract_token_usage helper; when a
provider does not report usage the fields stay null (never estimated).
- consumers/consumers_graph create_prompt_metric + finish_prompt_metric
now accept and persist optional tokens_in / tokens_out.
- Admin: PromptMetric lists tokens_in/tokens_out (+ event/model filters);
Conversation admin shows summed tokens_in/out/total per conversation.
- Tests cover the model fields, extract_token_usage parsing, metric
lifecycle token persistence, and admin aggregation.
Follow-ups: #16 (frontend token display) and #17 (billing/quotas).
Co-authored-by: Cursor <cursoragent@cursor.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #15
Summary
tokens_in/tokens_outIntegerFields toPromptMetricto record real prompt/completion token counts per turn.extract_token_usage()helper parses provider usage payloads (LangChainusage_metadata, OpenAI-styleprompt_tokens/completion_tokens, Ollamaprompt_eval_count/eval_count). When a provider reports no usage, the fields stay null — counts are never estimated/fabricated.create_prompt_metric/finish_prompt_metricin bothconsumers.pyandconsumers_graph.pyaccept and persist optionaltokens_in/tokens_out(added toupdate_fieldsonly when present).PromptMetricAdminliststokens_in/tokens_outand addsevent/model_name/has_filefilters.ConversationAdminshows summedtokens_in/tokens_out/tokens_totalper conversation.0023_promptmetric_tokens_in_promptmetric_tokens_out(existing rows remain valid — null).Note on live capture
The streaming chat path uses LangChain
StrOutputParser, which yields plain string chunks with no usage metadata, so live turns currently persistnulltokens (honest, per acceptance criteria — no fabricated counts). The plumbing + helper are in place so wiring real provider usage is a drop-in once the services expose it.Follow-ups
Test plan
uv run python manage.py test— full suite green (266 tests, 6 skipped)extract_token_usage: LangChain / OpenAI / Ollama key variants, attribute sources, bool/float handling, missing usage → (None, None)