mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(gemini): raise incomplete transcription interactions as non-retryable 400 and drop response bodies from Vertex transcription errors
This commit is contained in:
parent
e5a17a1032
commit
59eb7741ee
6 changed files with 63 additions and 3 deletions
|
|
@ -38,6 +38,7 @@ from litellm.types.utils import (
|
|||
INTERACTIONS_API_REVISION: Final = "2026-05-20"
|
||||
WORD_INFO_ANNOTATION_TYPE: Final = "word_info"
|
||||
_DECODED_JSON: Final = TypeAdapter(object)
|
||||
_RETRYABLE_INTERACTION_STATUSES: Final = frozenset({"failed"})
|
||||
|
||||
|
||||
class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
||||
|
|
@ -143,7 +144,7 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
parsed: Final = GeminiTranscriptionInteractionResponse.model_validate(response_json)
|
||||
if parsed.status != "completed":
|
||||
raise GeminiError(
|
||||
status_code=raw_response.status_code,
|
||||
status_code=500 if parsed.status in _RETRYABLE_INTERACTION_STATUSES else 400,
|
||||
message=f"Gemini transcription interaction did not complete (status={parsed.status})",
|
||||
)
|
||||
text_contents: Final = tuple(
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ class VertexGeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig, VertexB
|
|||
except ValueError:
|
||||
raise VertexAIError(
|
||||
status_code=raw_response.status_code,
|
||||
message=f"Received non-JSON response from Vertex AI Gemini transcription: {raw_response.text}",
|
||||
message="Received non-JSON response from Vertex AI Gemini transcription",
|
||||
)
|
||||
parsed: Final = VertexGeminiTranscriptionResponse.model_validate(response_json)
|
||||
texts: Final = tuple(
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ class VertexAIAudioTranscriptionConfig(BaseAudioTranscriptionConfig, VertexBase)
|
|||
except ValueError:
|
||||
raise VertexAIError(
|
||||
status_code=raw_response.status_code,
|
||||
message=f"Received non-JSON response from Google Speech-to-Text: {raw_response.text}",
|
||||
message="Received non-JSON response from Google Speech-to-Text",
|
||||
)
|
||||
parsed: Final = VertexSpeechToTextRecognizeResponse.model_validate(response_json)
|
||||
transcripts: Final = tuple(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import json
|
|||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.llms.gemini.audio_transcription.transformation import (
|
||||
|
|
@ -264,6 +265,52 @@ class TestTransformResponse:
|
|||
assert response.get("duration") is None
|
||||
|
||||
|
||||
|
||||
class TestNonCompletedInteractionThroughRouter:
|
||||
@pytest.fixture
|
||||
def router(self, monkeypatch):
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
return litellm.Router(
|
||||
model_list=[
|
||||
{
|
||||
"model_name": "transcribe",
|
||||
"litellm_params": {"model": "gemini/gemini-3.5-transcribe", "api_key": "test-gemini-key"},
|
||||
}
|
||||
],
|
||||
num_retries=2,
|
||||
)
|
||||
|
||||
def _interaction(self, status):
|
||||
payload = json.loads(json.dumps(COMPLETED_RESPONSE))
|
||||
payload["status"] = status
|
||||
payload["steps"][0]["content"][0]["text"] = "sensitive transcript text"
|
||||
return payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_incomplete_interaction_fails_once_as_bad_request_without_transcript(self, router):
|
||||
route = respx.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
|
||||
return_value=httpx.Response(200, json=self._interaction("incomplete"))
|
||||
)
|
||||
with pytest.raises(litellm.BadRequestError) as excinfo:
|
||||
await router.atranscription(model="transcribe", file=("call.wav", AUDIO_BYTES, "audio/wav"))
|
||||
assert route.call_count == 1
|
||||
assert excinfo.value.status_code == 400
|
||||
assert "status=incomplete" in str(excinfo.value)
|
||||
assert "sensitive transcript text" not in str(excinfo.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_failed_interaction_is_retried(self, router):
|
||||
route = respx.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
|
||||
return_value=httpx.Response(200, json=self._interaction("failed"))
|
||||
)
|
||||
with pytest.raises(litellm.InternalServerError) as excinfo:
|
||||
await router.atranscription(model="transcribe", file=("call.wav", AUDIO_BYTES, "audio/wav"))
|
||||
assert route.call_count == 3
|
||||
assert "sensitive transcript text" not in str(excinfo.value)
|
||||
|
||||
|
||||
class TestSubtitleSynthesisThroughHandler:
|
||||
def _transform(self, config, response_format):
|
||||
from unittest.mock import Mock
|
||||
|
|
|
|||
|
|
@ -208,6 +208,12 @@ class TestTransformResponse:
|
|||
response = config.transform_audio_transcription_response(raw_response)
|
||||
assert response.text == ""
|
||||
|
||||
def test_non_json_error_omits_response_body(self, config):
|
||||
raw_response = httpx.Response(status_code=200, text='{"results": [{"alternatives": [{"transcript": "sensitive')
|
||||
with pytest.raises(VertexAIError, match="non-JSON") as excinfo:
|
||||
config.transform_audio_transcription_response(raw_response)
|
||||
assert "sensitive" not in str(excinfo.value)
|
||||
|
||||
def test_fractional_billed_duration(self, config):
|
||||
raw_response = httpx.Response(
|
||||
status_code=200,
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ class TestTransformResponse:
|
|||
with pytest.raises(VertexAIError, match="non-JSON"):
|
||||
config.transform_audio_transcription_response(raw_response)
|
||||
|
||||
def test_non_json_error_omits_response_body(self, config):
|
||||
raw_response = httpx.Response(status_code=200, text='{"candidates": [{"content": {"parts": [{"text": "sensitive')
|
||||
with pytest.raises(VertexAIError) as excinfo:
|
||||
config.transform_audio_transcription_response(raw_response)
|
||||
assert "sensitive" not in str(excinfo.value)
|
||||
|
||||
|
||||
class TestValidateEnvironment:
|
||||
def test_sets_oauth_headers(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue