Merge pull request #38457 from BerriAI/litellm_realtime_audio_output_tokens

fix(realtime): bill Gemini Live native-audio output tokens at the audio rate
This commit is contained in:
Mateo Wang 2026-08-27 15:23:46 -07:00 committed by GitHub
commit 0c0a1f4eb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 0 deletions

View file

@ -2661,6 +2661,7 @@ class LiteLLMCompletionResponsesConfig:
optional_output_details: Final[dict[str, int]] = {
field: value
for field, value in (
("audio_tokens", getattr(completion_details, "audio_tokens", None)),
("text_tokens", getattr(completion_details, "text_tokens", None)),
("image_tokens", getattr(completion_details, "image_tokens", None)),
)

View file

@ -1258,6 +1258,8 @@ class ResponsesAPIRequestParams(ResponsesAPIOptionalRequestParams, total=False):
class OutputTokensDetails(BaseLiteLLMOpenAIResponseObject):
audio_tokens: int | None = None
reasoning_tokens: int | None = None
text_tokens: int | None = None

View file

@ -1866,6 +1866,54 @@ def test_map_openai_params_drops_stock_voice_case_insensitively():
assert passthrough["generationConfig"]["speechConfig"]["voiceConfig"]["prebuiltVoiceConfig"]["voiceName"] == "Kore"
def test_gemini_response_done_bills_audio_output_tokens_at_audio_rate(monkeypatch):
"""Regression for the Gemini Live AUDIO output breakdown: responseTokensDetails
must survive into response.done usage and bill at output_cost_per_audio_token,
not the text rate."""
from litellm.cost_calculator import (
RealtimeAPITokenUsageProcessor,
handle_realtime_stream_cost_calculation,
)
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url=""))
config = GeminiRealtimeConfig()
done_event = config.transform_response_done_event(
message={
"serverContent": {"turnComplete": True},
"usageMetadata": {
"promptTokenCount": 377,
"responseTokenCount": 51,
"totalTokenCount": 428,
"promptTokensDetails": [{"modality": "TEXT", "tokenCount": 377}],
"responseTokensDetails": [{"modality": "AUDIO", "tokenCount": 51}],
"thoughtsTokenCount": 37,
},
},
current_response_id="resp_lit6277",
current_conversation_id="conv_lit6277",
output_items=None,
)
usage = done_event["response"]["usage"]
assert usage["output_tokens_details"]["audio_tokens"] == 51
assert usage["output_token_details"]["audio_tokens"] == 51
results = [done_event]
combined_usage = RealtimeAPITokenUsageProcessor.collect_and_combine_usage_from_realtime_stream_results(
results=results,
)
assert combined_usage.completion_tokens_details is not None
assert combined_usage.completion_tokens_details.audio_tokens == 51
cost = handle_realtime_stream_cost_calculation(
results=results,
combined_usage_object=combined_usage,
custom_llm_provider="gemini",
litellm_model_name="gemini-2.5-flash-native-audio-preview-12-2025",
)
assert cost == pytest.approx(377 * 5e-07 + 51 * 1.2e-05 + 37 * 2e-06)
@pytest.fixture(autouse=False)
def patch_gemini_transcribe_live_cost_map_entry(monkeypatch):
"""Inject the gemini-3.5-transcribe-live registry entry locally.