mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(audio-transcription): keep reply bodies out of watsonx, whisper, xai and validation errors
Watsonx and whisper no longer print the parsed reply after Got: when it has no text field. TranscriptionResponse and the xAI response model hide input in validation errors, so a malformed field cannot quote transcript fragments. Deepgram and ElevenLabs keep the original error text, which is now safe.
This commit is contained in:
parent
f05d67b000
commit
17e7686beb
12 changed files with 75 additions and 23 deletions
|
|
@ -158,7 +158,7 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
return response
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error transforming Deepgram response: {type(e).__name__}") from None
|
||||
raise ValueError(f"Error transforming Deepgram response: {e}") from None
|
||||
|
||||
def _reconstruct_diarized_transcript(self, words: list) -> str:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
return response
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error transforming ElevenLabs response: {type(e).__name__}") from None
|
||||
raise ValueError(f"Error transforming ElevenLabs response: {e}") from None
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -141,7 +141,4 @@ class OpenAIWhisperAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
|
|||
if any(key in raw_response_json for key in TranscriptionResponse.model_fields):
|
||||
return TranscriptionResponse(**raw_response_json)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Invalid response format. Received response does not match the expected format. Got: ",
|
||||
raw_response_json,
|
||||
)
|
||||
raise ValueError("Invalid response format. Received response does not match the expected format.")
|
||||
|
|
|
|||
|
|
@ -191,10 +191,7 @@ class IBMWatsonXAudioTranscriptionConfig(IBMWatsonXMixin, OpenAIWhisperAudioTran
|
|||
response_kwargs["usage"] = usage
|
||||
|
||||
if not response_kwargs:
|
||||
raise ValueError(
|
||||
"Invalid response format. Received response does not match the expected format. Got: ",
|
||||
response_object,
|
||||
)
|
||||
raise ValueError("Invalid response format. Received response does not match the expected format.")
|
||||
|
||||
response: Final = TranscriptionResponse(**response_kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class _XAISttWord(LiteLLMBaseModel):
|
|||
|
||||
|
||||
class _XAISttResponse(LiteLLMBaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
model_config = ConfigDict(extra="allow", hide_input_in_errors=True)
|
||||
text: str = ""
|
||||
language: str = "unknown"
|
||||
duration: float | None = None
|
||||
|
|
|
|||
|
|
@ -2984,6 +2984,8 @@ _DETECTED_LANGUAGES: Final = TypeAdapter(tuple[TranscriptionDetectedLanguage, ..
|
|||
|
||||
|
||||
class TranscriptionResponse(OpenAIObject):
|
||||
model_config = ConfigDict(hide_input_in_errors=True)
|
||||
|
||||
text: str | None = None
|
||||
usage: TranscriptionUsageDurationObject | TranscriptionUsageTokensObject | None = None
|
||||
languages: Sequence[TranscriptionDetectedLanguage] | None = None
|
||||
|
|
|
|||
|
|
@ -601,18 +601,21 @@ def test_transform_response_keeps_the_transcript_out_of_malformed_words_errors(w
|
|||
@pytest.mark.parametrize(
|
||||
("payload", "error"),
|
||||
[
|
||||
({"results": None}, "TypeError"),
|
||||
(_deepgram_payload({"transcript": "hello", "words": 5}, {}), "TypeError"),
|
||||
({**_deepgram_payload({"transcript": "hello"}, {}), "metadata": None}, "TypeError"),
|
||||
(_deepgram_payload({"transcript": ["hello"]}, {}), "ValidationError"),
|
||||
({"results": None}, "'NoneType' object is not subscriptable"),
|
||||
(_deepgram_payload({"transcript": "hello", "words": 5}, {}), "object of type 'int' has no len()"),
|
||||
(
|
||||
{**_deepgram_payload({"transcript": "hello"}, {}), "metadata": None},
|
||||
"'NoneType' object is not subscriptable",
|
||||
),
|
||||
(_deepgram_payload({"transcript": ["hello"]}, {}), "1 validation error for TranscriptionResponse\ntext\n"),
|
||||
],
|
||||
)
|
||||
def test_transform_response_names_only_the_failure_type_of_a_malformed_body(payload: object, error: str):
|
||||
def test_transform_response_wraps_the_error_of_the_failing_lookup_without_the_body(payload: object, error: str):
|
||||
raw_response: Final = httpx.Response(200, json=payload)
|
||||
|
||||
with pytest.raises(ValueError, match="Error transforming Deepgram response: ") as exc_info:
|
||||
DeepgramAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
|
||||
|
||||
assert exc_info.type is ValueError
|
||||
assert str(exc_info.value) == f"Error transforming Deepgram response: {error}"
|
||||
assert str(exc_info.value).startswith(f"Error transforming Deepgram response: {error}")
|
||||
assert "hello" not in "".join(traceback.format_exception(exc_info.value))
|
||||
|
|
|
|||
|
|
@ -129,6 +129,15 @@ def test_transform_audio_transcription_response_keeps_the_body_out_of_malformed_
|
|||
assert "sensitive" not in "".join(traceback.format_exception(exc_info.value))
|
||||
|
||||
|
||||
def test_transform_audio_transcription_response_names_the_failing_lookup_of_a_malformed_payload():
|
||||
raw_response = httpx.Response(200, json={"text": "sensitive", "words": [None]})
|
||||
|
||||
with pytest.raises(ValueError, match=r"^Error transforming ElevenLabs response: 1 validation error for ") as exc_info:
|
||||
ElevenLabsAudioTranscriptionConfig().transform_audio_transcription_response(raw_response=raw_response)
|
||||
|
||||
assert "sensitive" not in "".join(traceback.format_exception(exc_info.value))
|
||||
|
||||
|
||||
class TestElevenLabsAudioTranscription:
|
||||
@pytest.mark.usefixtures("elevenlabs_api_key_env")
|
||||
def test_elevenlabs_diarize_parameter_passthrough(self):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from unittest.mock import MagicMock
|
|||
import httpx
|
||||
import litellm
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from litellm.llms.base_llm.audio_transcription.transformation import (
|
||||
BaseAudioTranscriptionConfig,
|
||||
|
|
@ -202,3 +203,13 @@ def test_mistral_audio_transcription_response_transform_keeps_non_json_body_out_
|
|||
MistralAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
|
||||
|
||||
assert "sensitive" not in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [["sensitive"], {"value": "sensitive"}])
|
||||
def test_mistral_audio_transcription_response_transform_keeps_a_malformed_text_field_out_of_the_error(text: object):
|
||||
raw_response = httpx.Response(200, json={"text": text, "segments": [{"text": "sensitive"}]})
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MistralAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
|
||||
|
||||
assert "sensitive" not in str(exc_info.value)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ and transform_audio_transcription_response.
|
|||
|
||||
import io
|
||||
import json
|
||||
import traceback
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from litellm.llms.openai.transcriptions.whisper_transformation import (
|
||||
|
|
@ -104,3 +106,10 @@ class TestWhisperTransformResponse:
|
|||
is_json=False,
|
||||
)
|
||||
)
|
||||
|
||||
def test_json_object_without_transcription_fields_keeps_the_body_out_of_the_error(self):
|
||||
config = OpenAIWhisperAudioTranscriptionConfig()
|
||||
raw_response = httpx.Response(200, json={"segments": [{"text": "sensitive"}]})
|
||||
with pytest.raises(ValueError, match="Invalid response format") as exc_info:
|
||||
config.transform_audio_transcription_response(raw_response)
|
||||
assert "sensitive" not in "".join(traceback.format_exception(exc_info.value))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Tests for IBM WatsonX Audio Transcription.
|
|||
Validates the WatsonX transcription response transformation.
|
||||
"""
|
||||
|
||||
import traceback
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
|
|
@ -120,17 +121,21 @@ def test_transform_audio_transcription_response_copies_every_field_except_model(
|
|||
assert {key: response[key] for key in expected_extras} == expected_extras
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [{}, {"model": "whisper"}, {"duration": 2.0}, {"text": None, "usage": None}])
|
||||
def test_transform_audio_transcription_response_without_text_or_usage_reports_the_body(
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{"model": "sensitive"},
|
||||
{"duration": 2.0, "segments": [{"text": "sensitive"}]},
|
||||
{"text": None, "usage": None, "note": "sensitive"},
|
||||
],
|
||||
)
|
||||
def test_transform_audio_transcription_response_without_text_or_usage_keeps_the_body_out_of_the_error(
|
||||
payload: dict[str, object],
|
||||
) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid response format") as exc_info:
|
||||
_transform_transcription_response(payload)
|
||||
|
||||
assert exc_info.value.args == (
|
||||
"Invalid response format. Received response does not match the expected format. Got: ",
|
||||
payload,
|
||||
)
|
||||
assert "sensitive" not in "".join(traceback.format_exception(exc_info.value))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("payload", [["not", "an", "object"], "plain text", 7, True])
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import traceback
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
|
|
@ -146,6 +148,23 @@ def test_transform_response_raises_on_error_status():
|
|||
assert "Incorrect API key provided" in exc.value.message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"content",
|
||||
[
|
||||
b'{"text": ["sensitive"]}',
|
||||
b'{"text": "ok", "words": [{"text": ["sensitive"]}]}',
|
||||
b'{"text": "sensitive',
|
||||
],
|
||||
)
|
||||
def test_transform_response_keeps_a_malformed_body_out_of_the_error(content):
|
||||
raw = httpx.Response(200, content=content, request=httpx.Request("POST", "https://api.x.ai/v1/stt"))
|
||||
|
||||
with pytest.raises(XAIAudioTranscriptionError, match="Error parsing xAI response") as exc:
|
||||
CONFIG.transform_audio_transcription_response(raw_response=raw)
|
||||
|
||||
assert "sensitive" not in "".join(traceback.format_exception(exc.value))
|
||||
|
||||
|
||||
def test_transcription_routes_to_xai_stt(monkeypatch):
|
||||
monkeypatch.delenv("XAI_API_KEY", raising=False)
|
||||
monkeypatch.setattr(litellm, "xai_key", None)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue