feat: add opt-in estimated API cost reporting - #2817
Conversation
PR Summary by QodoAdd opt-in estimated API cost reporting
AI Description
Diagram
High-Level Assessment
Files changed (11)
|
Code Review by Qodo
1.
|
IsmaelMartinez
left a comment
There was a problem hiding this comment.
This is good work. Making "we couldn't price this call" and "this call cost zero" two different things is exactly right.
One thing to fix before it goes in. Qodo found it; I've confirmed it and left the change as three suggestions.
You can ignore Qodo's note about the root .pr_agent.toml needing a matching key. That file isn't a copy of configuration.toml: it has no [config] section at all, and it has a [review_agent] section this package doesn't even have. Your setting is in the right place.
|
Follow-up to the Qodo review in commit b3ad13d:
Verification:
|
|
Code review by qodo was updated up to the latest commit b3ad13d |
litellm.completion_cost returns 0.0 rather than raising both for zero-priced model_cost entries (local/ollama models) and for usage without billable tokens. _as_decimal_cost accepted any non-negative value, so those calls were counted as priced and the run rendered 'Estimated API cost: $0.0000 USD' with cost status complete — the exact false zero the cost feature promises never to show. Reject cost <= 0 so such calls fall to partial/unavailable, matching the existing > 0 rule for inline response costs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
_has_priceable_usage walked the whole usage object recursively and accepted any positive number as a billable quantity, so provider extras like Groq's queue_time/prompt_time floats passed the gate even with zero prompt and completion tokens, sending unpriceable usage to litellm.completion_cost (which returns 0.0 for it instead of raising). Check the token counters directly instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
The PR introduced _response_field in litellm_helpers yet inlined three more copies of the same dict-vs-attribute accessor, and duplicated run_details' Decimal validation in _read_positive_response_cost with only a >= vs > difference — the drift behind the false-$0 bug fixed earlier. Use the shared helper and run_details._as_decimal_cost (which now enforces > 0) so there is one implementation of each rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
The try/except wrapped only litellm.completion_cost, leaving the model_dump() probes in _read_positive_response_cost and _has_priceable_usage unguarded — an exotic response object could raise out of _record_completion_metadata and discard a successful, already-billed completion. Widen the guard to the whole cost block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
kwargs is built entirely inside chat_completion and the extra-body
allowlist admits only processing_mode/service_tier, so
kwargs.get('stream_options') is always None here; assign the dict
directly instead of merging with a value that cannot exist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
The two force-streaming tests still stubbed _handle_streaming_response with the removed 2-tuple shape, so the new (content, finish_reason, completed_response) contract had no coverage on that path. Return the 3-tuple, assert it is passed through, and pin the stream_options and model= kwargs the streaming branch now sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
'unavailable (priceable usage was not available)' asserted a cause that is often wrong: the openai and langchain handlers record calls (with usage) but have no pricing wired up, and their runs would claim usage was missing. Say 'no calls could be priced' instead, and document at the OpenAIHandler call site why it records no cost, mirroring the langchain handler's inline note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
In IMDS mode the Bedrock lock serializes every concurrent call to protect the os.environ credential swap. Debug logging, prepare_logs, and the new synchronous cost pricing in _record_completion_metadata need none of that state, yet ran while holding the lock, so every waiting coroutine paid for them serially. Move the post-response bookkeeping after the lock releases; the variables it reads stay in scope past the with block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfjC52hJptWtP5QXRJwVKT
|
Code review by qodo was updated up to the latest commit 5f3db32 |
|
Code review by qodo was updated up to the latest commit dac9292 |
|
Code review by qodo was updated up to the latest commit 1d78c26 |
|
@IsmaelMartinez this works in my env. Need any additional changes before you merge it? Thanks! |
IsmaelMartinez
left a comment
There was a problem hiding this comment.
Nothing further needed, and display_model is a better name than the one I suggested.
I checked the split does what we wanted: completion_cost still prices on the routed name, and only the label takes the configured one. Suite is green on your head merged onto today's main.
Good to merge from my side.
Summary
Streaming reliability
Some models, including anthropic/claude-opus-5, are forced through LiteLLM streaming. An asynchronous callback or callback kwargs response_cost is not sufficient for these calls: callback delivery can lag the completed request, and the field can be absent or still contain a zero placeholder when the stream is consumed.
This change requests finalized stream usage, retains the real usage object on the completed streaming response, and collects cost synchronously. It first accepts a positive finalized inline response cost when available; otherwise it calls LiteLLM completion_cost with the completed response and full usage object. That lets LiteLLM price cache reads, cache writes, reasoning tokens, and provider-specific categories it understands.
If a completed call has no finalized priceable usage, or LiteLLM cannot price it, the call remains successful but unpriced. Aggregates become partial or unavailable instead of treating missing data as zero. Cost aggregation retains exact Decimal values; public currency output is rounded to two decimal places, and tiny positive values that would round to zero render as less than $0.01 rather than a false $0.00.
Safety and compatibility
Verification