From aec7a12f33b783931b407876bc382e432dc44861 Mon Sep 17 00:00:00 2001 From: xiaoyuyu6420 <93528429+xiaoyuyu6420@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:07:48 +0800 Subject: [PATCH 1/2] fix(provider): exclude cached tokens from Gemini input usage accounting Gemini's prompt_token_count already includes tokens served from cache. _extract_usage was passing it straight to TokenUsage.input_other while also setting input_cached to cached_content_token_count, so usage.input double counted cached tokens and inflated context-occupancy stats. Subtract cached tokens from input_other, matching the OpenAI provider's accounting. --- .../core/provider/sources/gemini_source.py | 13 +++++-- tests/test_gemini_source.py | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index f4cba79805..93a775636e 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -451,10 +451,17 @@ def _extract_reasoning_content(self, candidate: types.Candidate) -> str: def _extract_usage( self, usage_metadata: types.GenerateContentResponseUsageMetadata ) -> TokenUsage: - """Extract usage from candidate""" + """Extract usage from candidate. + + `prompt_token_count` includes tokens served from cache, so subtract + `cached_content_token_count` to avoid double-counting cached input + (matching the OpenAI provider's TokenUsage accounting). + """ + prompt_tokens = usage_metadata.prompt_token_count or 0 + cached = usage_metadata.cached_content_token_count or 0 return TokenUsage( - input_other=usage_metadata.prompt_token_count or 0, - input_cached=usage_metadata.cached_content_token_count or 0, + input_other=prompt_tokens - cached, + input_cached=cached, output=usage_metadata.candidates_token_count or 0, ) diff --git a/tests/test_gemini_source.py b/tests/test_gemini_source.py index 4c33de6bca..d33dafd449 100644 --- a/tests/test_gemini_source.py +++ b/tests/test_gemini_source.py @@ -131,6 +131,42 @@ def test_gemini_reasoning_only_output_is_allowed(): ) +def test_gemini_extract_usage_excludes_cached_tokens_from_input_other(): + provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI) + + usage_metadata = SimpleNamespace( + prompt_token_count=100, + cached_content_token_count=30, + candidates_token_count=50, + ) + + usage = provider._extract_usage(usage_metadata) + + # prompt_token_count already includes cached tokens; input_other must + # exclude them so input (input_other + input_cached) is not inflated. + assert usage.input_other == 70 + assert usage.input_cached == 30 + assert usage.input == 100 + assert usage.output == 50 + + +def test_gemini_extract_usage_without_cache_keeps_full_prompt_tokens(): + provider = ProviderGoogleGenAI.__new__(ProviderGoogleGenAI) + + usage_metadata = SimpleNamespace( + prompt_token_count=100, + cached_content_token_count=0, + candidates_token_count=20, + ) + + usage = provider._extract_usage(usage_metadata) + + assert usage.input_other == 100 + assert usage.input_cached == 0 + assert usage.input == 100 + assert usage.output == 20 + + @pytest.mark.asyncio async def test_gemini_get_models_retries_transient_request_error(monkeypatch): monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0) From 5a6be73353901e310e97399fb70fdbda993c7de1 Mon Sep 17 00:00:00 2001 From: xiaoyuyu6420 <93528429+xiaoyuyu6420@users.noreply.github.com> Date: Tue, 1 Sep 2026 08:52:43 +0800 Subject: [PATCH 2/2] docs(provider): clarify _extract_usage reads response usage metadata Address Sourcery review: the method receives GenerateContentResponseUsageMetadata, not a candidate, so correct the docstring's opening line. --- astrbot/core/provider/sources/gemini_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 93a775636e..d8ec6dbffc 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -451,7 +451,7 @@ def _extract_reasoning_content(self, candidate: types.Candidate) -> str: def _extract_usage( self, usage_metadata: types.GenerateContentResponseUsageMetadata ) -> TokenUsage: - """Extract usage from candidate. + """Extract usage from response metadata. `prompt_token_count` includes tokens served from cache, so subtract `cached_content_token_count` to avoid double-counting cached input