fix(cost): fold reasoning tokens into calc_cost - #65
Merged
Conversation
AhmadHammad21
force-pushed
the
fm/odo-cost-x4
branch
from
June 25, 2026 20:42
1a9fba9 to
4e3120a
Compare
calc_cost() priced only input + output tokens, so for providers that report reasoning/thinking tokens *outside* output_tokens (e.g. Gemini 2.5 Flash via OpenRouter) the cost was undercounted ~10x even though reasoning tokens were already captured for display. calc_cost now bills reasoning tokens at the output rate, adding them only when reasoning_tok > output_tok — this corrects the non-compliant providers without double-counting compliant ones (gpt-oss already folds reasoning into output_tokens). chat.py and save_turn pass the captured reasoning_tokens through. Part B (follow-up-questions UI) verified already wired in ChatPage.tsx (state + done-event capture + clickable chips); no change needed. Adds tests/test_agent/test_cost.py covering the undercount case, the no-double-count case, and the no-reasoning passthrough. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AhmadHammad21
force-pushed
the
fm/odo-cost-x4
branch
from
June 25, 2026 20:45
4e3120a to
117f94a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix the cost-accounting undercount tracked as open issue #59 (Phase 0 of round-2 feedback), and verify the follow-up-questions UI is wired.
Part A (the bug): calc_cost() in apps/core/.../agent/turns.py priced only input + output tokens. Reasoning/thinking tokens were already captured for display in chat.py but never folded into the cost math, so for providers that report reasoning tokens OUTSIDE output_tokens (e.g. Gemini 2.5 Flash via OpenRouter) cost was undercounted ~10x. Fix: calc_cost gained a reasoning_tok param and bills reasoning at the OUTPUT rate. The fold is intentionally detection-based — reasoning is added only when reasoning_tok > output_tok. This is deliberate: per the LangChain usage_metadata standard, output_token_details.reasoning is a SUBSET of output_tokens, so compliant providers (e.g. gpt-oss, which reports 53 output incl. 38 reasoning) already include it and must NOT be double-counted; non-compliant providers (Gemini reports output=0/6 with reasoning separate) need it added. Both callers (save_turn in turns.py and the inline calc in chat.py) pass the captured reasoning_tokens through; the summarizer caller is unaffected via the default of 0. Pricing comes from existing config (litellm.model_cost / _FALLBACK_PRICING) — no magic numbers.
Part B (quick win, verify-only): the agent already emits follow_up_questions via submit_investigation. I verified the frontend already fully renders them in ChatPage.tsx (state + done-event capture + clickable chips that call send(q)). No code change was needed for Part B — this is intentional.
Tests: added apps/backend/tests/test_agent/test_cost.py covering the undercount case (reasoning folded), the no-double-count case (gpt-oss style), no-reasoning passthrough, and the real Gemini model. Also promoted CLAUDE.md to AGENTS.md (via the project's ensure script; CLAUDE.md is now a symlink) and added a concise durable note documenting the cost-folding contract. No changes to the bash allowlist, SSE event types, or anything in the CLAUDE.md 'What NOT to Change' table.
What Changed
calc_cost()inapps/core/.../agent/turns.pygains areasoning_tokparameter and bills reasoning tokens at the output rate, folding them intobillable_outputonly whenreasoning_tok > output_tok— so providers that report reasoning outsideoutput_tokens(e.g. Gemini 2.5 Flash via OpenRouter) are no longer undercounted, while compliant providers (gpt-oss, where reasoning is a subset ofoutput_tokens) are not double-counted. Both callers —save_turnand the inline cost calc inrouters/chat.py— pass the capturedreasoning_tokensthrough.apps/backend/tests/test_agent/test_cost.pycovering the undercount fold, the no-double-count case, no-reasoning passthrough, and the real Gemini model.apps/documentation/benchmark.mdto reflect that the reasoning-token undercount is fixed, and promotedCLAUDE.mdtoAGENTS.md(CLAUDE.md is now a symlink) with a note documenting the cost-folding contract.Risk Assessment
✅ Low: A tightly-scoped, correct bug fix to cost accounting with mirrored logic across both pricing branches, both call sites updated consistently, focused test coverage, and a clean docs move (verified content-identical).
Testing
Baseline plus targeted tests all pass: the 4 new cost tests and the full test_agent suite (11 tests) are green. Beyond unit assertions, I exercised calc_cost end-to-end with the real Gemini 2.5 Flash model and a fallback-priced model, capturing a CLI transcript that shows the actual reported cost rising when reasoning tokens reported outside output_tokens are folded in, and staying identical for the gpt-oss subset case (no double-count) — directly demonstrating the issue #59 fix as it would surface in the chat cost card. Part B is verify-only: I confirmed in ChatPage.tsx that follow_up_questions are captured from the done SSE event and rendered as clickable chips that re-send the question, matching the author's stated no-code-change intent. No reviewer-visible visual artifact was captured for the chip UI since exercising it requires a live LLM-backed investigation that emits submit_investigation with follow-ups; the wiring was verified by source inspection instead.
Evidence: calc_cost end-to-end demo (Gemini undercount fold + gpt-oss no-double-count)
Gemini 2.5 Flash (input=14000,output=6,reasoning=2000): BEFORE $0.00421500 -> AFTER $0.00921500 (reasoning folded). gpt-oss style (output=53 incl. 38 reasoning): with/without reasoning arg both $0.00008855 (equal, no double-count).Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
apps/core/src/opendevops_core/agent/turns.py:29- The detection heuristicreasoning_tok > output_tokcorrectly handles the documented Gemini case (output≈0/6, reasoning separate) and gpt-oss case (reasoning subset of output). It silently leaves an undercount for a hypothetical non-compliant provider that reports reasoning outside output_tokens but with reasoning_tok <= output_tok (e.g. visible output 6, separate reasoning 4). There is no way to disambiguate this without per-provider knowledge, and the intent documents it as a deliberate tradeoff — flagging only so the limitation is on record, not as a defect to fix.✅ **Test** - passed
✅ No issues found.
uv run pytest tests/test_agent/test_cost.py -v— 4 cost-fold tests passuv run pytest tests/test_agent/ -q— full agent suite (11) passes, no regressionRan calc_cost end-to-end against realopenrouter/google/gemini-2.5-flashand the gpt-oss-style fallback model to demonstrate the cost-card number changes (reasoning folded vs. not double-counted)Inspectedapps/frontend/src/pages/ChatPage.tsx— confirmed follow_up_questions captured fromdoneevent and rendered as clickable chips calling send(q)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.