diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index f4cba79805..d8ec6dbffc 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 response metadata. + + `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)