From 4ffff0a57895a78fb559df3a3a2c059b62229cfa Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Mon, 19 Oct 2026 23:24:56 -0500 Subject: [PATCH] fix(realtime): preserve upstream authentication and transcription behavior --- litellm/cost_calculator.py | 15 +++++------ .../transcriptions/gpt_transformation.py | 1 - litellm/realtime_api/main.py | 4 ++- .../llms/openai/realtime/test_translation.py | 11 +++++--- .../transcriptions/test_gpt_transcribe.py | 2 +- tests/unit/realtime_api/test_main.py | 26 ++++++++++++++----- 6 files changed, 38 insertions(+), 21 deletions(-) diff --git a/litellm/cost_calculator.py b/litellm/cost_calculator.py index 8f2b1749be6..6ee11918760 100644 --- a/litellm/cost_calculator.py +++ b/litellm/cost_calculator.py @@ -1888,14 +1888,13 @@ def completion_cost( # the response attribute (for verbose_json responses that # naturally include duration from the provider). _hidden = getattr(completion_response, "_hidden_params", {}) or {} - provider_duration = _get_transcription_usage_duration(completion_response) - audio_transcription_file_duration = ( - provider_duration - if provider_duration is not None - else _hidden.get( - "audio_transcription_duration", - getattr(completion_response, "duration", 0.0), - ) + audio_transcription_file_duration = _hidden.get( + "audio_transcription_duration", + getattr(completion_response, "duration", 0.0), + ) or ( + (_get_transcription_usage_duration(completion_response) or 0.0) + if _bills_provider_transcription_seconds(requested_model, custom_llm_provider) + else 0.0 ) elif call_type in _RERANK_CALL_TYPES: if completion_response is not None and isinstance(completion_response, RerankResponse): diff --git a/litellm/llms/openai/transcriptions/gpt_transformation.py b/litellm/llms/openai/transcriptions/gpt_transformation.py index 99fbb6b96c8..dc072f72525 100644 --- a/litellm/llms/openai/transcriptions/gpt_transformation.py +++ b/litellm/llms/openai/transcriptions/gpt_transformation.py @@ -59,7 +59,6 @@ class OpenAIGPTTranscribeAudioTranscriptionConfig(OpenAIGPTAudioTranscriptionCon data: Final = { "model": model, "file": audio_file, - "response_format": "json", **optional_params, } return AudioTranscriptionRequestData(data=data) diff --git a/litellm/realtime_api/main.py b/litellm/realtime_api/main.py index 9821f46045f..f6e63e515cb 100644 --- a/litellm/realtime_api/main.py +++ b/litellm/realtime_api/main.py @@ -322,7 +322,9 @@ async def acreate_realtime_translation_client_secret( request_data["session"].pop("type", None) return await base_llm_http_handler.async_realtime_translation_client_secret_handler( api_base=resolved_api_base, - api_key=resolved_api_key, + api_key=await _realtime_http_bearer_token( + custom_llm_provider, resolved_api_key, resolved_api_base, litellm_params + ), request_data=request_data, logging_obj=litellm_logging_obj, timeout=timeout or request_timeout, diff --git a/tests/unit/llms/openai/realtime/test_translation.py b/tests/unit/llms/openai/realtime/test_translation.py index 06af49f26ca..97e9f822253 100644 --- a/tests/unit/llms/openai/realtime/test_translation.py +++ b/tests/unit/llms/openai/realtime/test_translation.py @@ -1,5 +1,6 @@ import gzip import json +from typing import Final from unittest.mock import AsyncMock, MagicMock import httpx @@ -78,14 +79,16 @@ def test_openai_translation_calls_url(): ) -def test_azure_translation_urls_use_ga_paths(): - config = AzureRealtimeHTTPConfig() +@pytest.mark.parametrize("suffix", ("", "/", "/openai", "/openai/v1/")) +def test_azure_translation_urls_use_ga_paths(suffix: str): + config: Final = AzureRealtimeHTTPConfig() + api_base: Final = f"https://example.openai.azure.com{suffix}" assert ( - config.get_translation_client_secret_url("https://example.openai.azure.com", "translate-deployment") + config.get_translation_client_secret_url(api_base, "translate-deployment") == "https://example.openai.azure.com/openai/v1/realtime/translations/client_secrets" ) assert ( - config.get_translation_calls_url("https://example.openai.azure.com", "translate-deployment") + config.get_translation_calls_url(api_base, "translate-deployment") == "https://example.openai.azure.com/openai/v1/realtime/translations/calls" ) diff --git a/tests/unit/llms/openai/transcriptions/test_gpt_transcribe.py b/tests/unit/llms/openai/transcriptions/test_gpt_transcribe.py index 535b52a17ee..ccc176e6451 100644 --- a/tests/unit/llms/openai/transcriptions/test_gpt_transcribe.py +++ b/tests/unit/llms/openai/transcriptions/test_gpt_transcribe.py @@ -38,7 +38,7 @@ def test_gpt_transcribe_config_uses_native_parameters_and_json(): request = config.transform_audio_transcription_request( model="gpt-transcribe", audio_file=audio_file, - optional_params={"keywords": ["LiteLLM"], "languages": ["en", "fr"], "stream": True}, + optional_params={"keywords": ["LiteLLM"], "languages": ["en", "fr"], "stream": True, "response_format": "json"}, litellm_params={}, ) assert request.data["response_format"] == "json" diff --git a/tests/unit/realtime_api/test_main.py b/tests/unit/realtime_api/test_main.py index b6aca93cd67..dac63bf1d8e 100644 --- a/tests/unit/realtime_api/test_main.py +++ b/tests/unit/realtime_api/test_main.py @@ -2,6 +2,7 @@ import asyncio import json import time from collections.abc import Awaitable, Callable, Mapping +from datetime import datetime, timezone from pathlib import Path from types import TracebackType from typing import Final @@ -13,6 +14,7 @@ import respx import litellm from litellm.integrations.custom_guardrail import CustomGuardrail +from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.realtime_streaming import RealTimeStreaming from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler from litellm.llms.openai.workload_identity import _workload_identity_auth @@ -1188,20 +1190,32 @@ class _GatewayThatRecordsTheAuthorization: @pytest.mark.asyncio @pytest.mark.parametrize( - "route", - [realtime_main.acreate_realtime_client_secret, realtime_main.acreate_realtime_transcription_session], - ids=["client_secrets", "transcription_sessions"], + ("route", "model"), + [ + (realtime_main.acreate_realtime_client_secret, "gpt-realtime"), + (realtime_main.acreate_realtime_transcription_session, "gpt-realtime"), + (realtime_main.acreate_realtime_translation_client_secret, "gpt-realtime-translate"), + ], + ids=["client_secrets", "transcription_sessions", "translation_client_secrets"], ) @respx.mock async def test_openai_realtime_http_routes_send_the_federated_bearer_for_a_keyless_deployment( - federated_deployment: dict[str, str], route: Callable[..., Awaitable[object]] + federated_deployment: dict[str, str], route: Callable[..., Awaitable[object]], model: str ) -> None: _mock_token_exchange() gateway: Final = _GatewayThatRecordsTheAuthorization() await route.__wrapped__( - model="gpt-realtime", + model=model, custom_llm_provider="openai", - litellm_logging_obj=FakeLogging(), + litellm_logging_obj=Logging( + model=model, + messages=[], + stream=False, + call_type="acreate_realtime_client_secret", + start_time=datetime(2026, 1, 1, tzinfo=timezone.utc), + litellm_call_id="test-federated-realtime", + function_id="test-federated-realtime", + ), client=AsyncHTTPHandler(transport=httpx.MockTransport(gateway)), **federated_deployment, )