-
Notifications
You must be signed in to change notification settings - Fork 48
feat(billing): measure the whole generation pipeline's real token cost #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -70,6 +70,7 @@ | |||||||||||||
| default_max_tokens_for_model, | ||||||||||||||
| resolve_model_name, | ||||||||||||||
| ) | ||||||||||||||
| from reflexio.server.llm.token_accounting import run_token_capture | ||||||||||||||
|
|
||||||||||||||
| if TYPE_CHECKING: | ||||||||||||||
| from reflexio.server.llm._litellm_types import LiteLLMConfig | ||||||||||||||
|
|
@@ -1194,6 +1195,23 @@ def _log_token_usage(self, params: dict[str, Any], response: Any) -> None: | |||||||||||||
| f", cache_write: {cache_creation or 0}, cache_read: {cache_read or 0}" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| # Accumulate into the run-scoped total, if a run installed one. This is | ||||||||||||||
| # the single chokepoint: `_completion_with_hard_timeout` is the only | ||||||||||||||
| # wrapper around `litellm.completion` in this module and has exactly one | ||||||||||||||
| # call site, three lines above the call to this method. Accumulating here | ||||||||||||||
| # rather than at 19 call sites means every future stage is counted by | ||||||||||||||
| # construction. `.get()` returns None outside a generation run (and in a | ||||||||||||||
| # worker whose context was not copied), where contributing nothing is the | ||||||||||||||
| # correct answer. | ||||||||||||||
| capture = run_token_capture.get() | ||||||||||||||
| if capture is not None: | ||||||||||||||
| capture.observe( | ||||||||||||||
| prompt_tokens=getattr(usage, "prompt_tokens", None), | ||||||||||||||
| completion_tokens=getattr(usage, "completion_tokens", None), | ||||||||||||||
| cache_read_input_tokens=cache_read, | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Capture nested cache-read tokens. When Use the top-level value when present. Otherwise, use the nested value. Select one value instead of adding both. Proposed fix- cache_read_input_tokens=cache_read,
+ cache_read_input_tokens=(
+ cache_read
+ if cache_read is not None
+ else getattr(details, "cached_tokens", None)
+ ),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| cache_write_input_tokens=cache_creation, | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| cost = self._compute_cost_usd(response, params.get("model")) | ||||||||||||||
| cost_suffix = f", cost: ${cost:.6f}" if cost is not None else "" | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Capture usage before response parsing.
When
_completion_with_hard_timeoutreturns a response with usage butchoicesis empty or malformed,_call_and_parsecan raise while readingresponse.choices[0].message._log_token_usageruns after that read, so it does not updaterun_token_capture. The error becomesLiteLLMClientError, and_make_requestcan advance to the fallback rung. The fallback usage is captured, but the first request is omitted from the accumulated run totals.Call
_log_token_usage(turn_params, response)immediately after_completion_with_hard_timeoutreturns. Keep provenance and response parsing after that call. This also captures usage for requests that enter the existing same-model retry paths.🤖 Prompt for AI Agents