fix(audio-transcription): keep transcripts out of transcription error messages (#50137)

Backport of #50137 to lts/1.108.x.
Cherry-picked from 5085022580 (main).

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
qtim0 2026-11-05 18:23:56 -08:00 • committed by mateo-berri
parent 93f52b591e
commit 79c6f14f4c
28 changed files with 288 additions and 43 deletions

View file

@ -995,9 +995,10 @@ def convert_to_model_response_object(
if isinstance(e, APIError):
raise
hide_reply: Final = response_type == "audio_transcription"
received_args: Final = dict(
response_object=response_object,
model_response_object=model_response_object,
response_object="<hidden>" if hide_reply else response_object,
model_response_object="<hidden>" if hide_reply else model_response_object,
response_type=response_type,
stream=stream,
start_time=start_time,

View file

@ -128,7 +128,7 @@ class DeepgramAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
return response
except Exception as e:
raise ValueError(f"Error transforming Deepgram response: {e}\nResponse: {raw_response.text}")
raise ValueError(f"Error transforming Deepgram response: {e}") from None
def _reconstruct_diarized_transcript(self, words: list) -> str:
"""

View file

@ -152,7 +152,7 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
return response
except Exception as e:
raise ValueError(f"Error transforming ElevenLabs response: {e}\nResponse: {raw_response.text}")
raise ValueError(f"Error transforming ElevenLabs response: {e}") from None
def get_complete_url(
self,

View file

@ -136,13 +136,13 @@ class GeminiAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
except ValueError:
raise GeminiError(
status_code=raw_response.status_code,
message=f"Received non-JSON response from Gemini Interactions API: {raw_response.text}",
message="Received non-JSON response from Gemini Interactions API",
)
parsed: Final = GeminiTranscriptionInteractionResponse.model_validate(response_json)
if parsed.status != "completed":
raise GeminiError(
status_code=raw_response.status_code,
message=f"Gemini transcription interaction did not complete (status={parsed.status}): {raw_response.text}",
message=f"Gemini transcription interaction did not complete (status={parsed.status})",
)
text_contents: Final = tuple(
content

View file

@ -133,7 +133,7 @@ class MistralAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
response_json: Final = raw_response.json()
except Exception:
raise MistralAudioTranscriptionException(
message=raw_response.text,
message="Received non-JSON response from Mistral audio transcription",
status_code=raw_response.status_code,
headers=raw_response.headers,
)

View file

@ -140,7 +140,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.")

View file

@ -144,7 +144,7 @@ class OVHCloudAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
response_json: Final = raw_response.json()
except Exception:
raise OVHCloudException(
message=raw_response.text,
message="Received non-JSON response from OVHCloud audio transcription",
status_code=raw_response.status_code,
headers=raw_response.headers,
)

View file

@ -134,7 +134,7 @@ class ScalewayAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
response_json: Final = raw_response.json()
except Exception:
raise ScalewayAudioTranscriptionException(
message=raw_response.text,
message="Received non-JSON response from Scaleway audio transcription",
status_code=raw_response.status_code,
headers=raw_response.headers,
)

View file

@ -180,7 +180,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(

View file

@ -171,7 +171,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(

View file

@ -174,7 +174,7 @@ class IBMWatsonXAudioTranscriptionConfig(IBMWatsonXMixin, OpenAIWhisperAudioTran
try:
raw_response_json: Final = raw_response.json()
except Exception as e:
raise ValueError(f"Error transforming response to json: {e}\nResponse: {raw_response.text}")
raise ValueError(f"Error transforming response to json: {e}")
# Extract only valid fields for TranscriptionResponse.__init__()
# TranscriptionResponse only accepts 'text' and 'usage' in __init__()
@ -190,10 +190,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)

View file

@ -38,7 +38,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

View file

@ -75,7 +75,7 @@ class GeminiTranscriptionUsage(LiteLLMBaseModel):
class GeminiTranscriptionInteractionResponse(LiteLLMBaseModel):
model_config = ConfigDict(extra="ignore")
model_config = ConfigDict(extra="ignore", hide_input_in_errors=True)
id: str | None = None
status: str | None = None

View file

@ -68,7 +68,7 @@ class VertexGeminiTranscriptionUsageMetadata(LiteLLMBaseModel):
class VertexGeminiTranscriptionResponse(LiteLLMBaseModel):
model_config = ConfigDict(extra="ignore")
model_config = ConfigDict(extra="ignore", hide_input_in_errors=True)
candidates: tuple[VertexGeminiTranscriptionCandidate, ...] = ()
usageMetadata: VertexGeminiTranscriptionUsageMetadata | None = None

View file

@ -40,6 +40,8 @@ class VertexSpeechToTextResponseMetadata(LiteLLMBaseModel):
class VertexSpeechToTextRecognizeResponse(LiteLLMBaseModel):
model_config = ConfigDict(hide_input_in_errors=True)
results: list[VertexSpeechToTextResult] = Field(default=[])
metadata: VertexSpeechToTextResponseMetadata | None = None

View file

@ -2848,6 +2848,8 @@ class TranscriptionUsageTokensObject(LiteLLMBaseModel):
class TranscriptionResponse(OpenAIObject):
model_config = ConfigDict(hide_input_in_errors=True)
text: str | None = None
usage: TranscriptionUsageDurationObject | TranscriptionUsageTokensObject | None = None

View file

@ -2695,6 +2695,24 @@ class TestConvertToModelResponseObjectAudioTranscription:
assert result.usage.output_tokens == 5
assert result.usage.input_token_details.audio_tokens == 4
def test_parse_error_keeps_the_transcript_out_of_the_exception(self):
import traceback
from litellm.types.utils import TranscriptionResponse
transcript: Final = "CANARYTRANSCRIPT customer account ending 2750"
with pytest.raises(Exception, match="Invalid response object") as exc_info:
convert_to_model_response_object(
response_object={"text": transcript, "usage": {"type": "duration", "seconds": "not-a-number"}},
model_response_object=TranscriptionResponse(),
response_type="audio_transcription",
)
rendered: Final = "".join(traceback.format_exception(exc_info.value))
assert "received_args=" in rendered
assert "CANARYTRANSCRIPT" not in rendered
class TestConvertToModelResponseObjectRerank:
def test_basic_rerank(self):
from litellm.types.utils import RerankResponse

View file

@ -1,10 +1,13 @@
import io
import os
import pathlib
import traceback
from typing import Final
from unittest.mock import MagicMock
import httpx
import pytest
from pydantic import ValidationError
from litellm.llms.base_llm.audio_transcription.transformation import AudioTranscriptionRequestData
from litellm.llms.deepgram.audio_transcription.transformation import (
@ -587,10 +590,33 @@ def test_transform_response_maps_words_to_openai_word_timestamps(words: object,
[{"word": "hello", "start": 0.0}],
],
)
def test_transform_response_wraps_malformed_words_with_the_raw_body(words: object):
def test_transform_response_keeps_the_transcript_out_of_malformed_words_errors(words: object):
raw_response = httpx.Response(200, json=_deepgram_payload({"transcript": "hello", "words": words}, {}))
with pytest.raises(ValueError, match="Error transforming Deepgram response: ") as exc_info:
DeepgramAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
assert str(exc_info.value).endswith(f"\nResponse: {raw_response.text}")
assert "hello" not in "".join(traceback.format_exception(exc_info.value))
@pytest.mark.parametrize(
("payload", "failing_lookup_error"),
[
({"results": None}, TypeError),
(_deepgram_payload({"transcript": "hello", "words": 5}, {}), TypeError),
({**_deepgram_payload({"transcript": "hello"}, {}), "metadata": None}, TypeError),
(_deepgram_payload({"transcript": ["hello"]}, {}), ValidationError),
],
)
def test_transform_response_wraps_the_error_of_the_failing_lookup_without_the_body(
payload: object, failing_lookup_error: type[Exception]
):
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)
failing_lookup: Final = exc_info.value.__context__
assert isinstance(failing_lookup, failing_lookup_error)
assert str(exc_info.value) == f"Error transforming Deepgram response: {failing_lookup}"
assert "hello" not in "".join(traceback.format_exception(exc_info.value))

View file

@ -1,9 +1,11 @@
import traceback
from collections.abc import Iterator
from typing import Final
import httpx
import pytest
import respx
from pydantic import ValidationError
import litellm
from litellm.caching.llm_caching_handler import LLMClientCache
@ -109,24 +111,35 @@ def test_transform_audio_transcription_response_without_words_key_has_no_word_li
@pytest.mark.parametrize(
"payload",
[
["not", "an", "object"],
"plain text",
7,
{"text": ["not", "text"]},
{"text": "t", "words": None},
{"text": "t", "words": 7},
{"text": "t", "words": "not a list"},
{"text": "t", "words": ["not an object"]},
{"text": "t", "words": [{"type": "word", "text": "ok"}, None]},
["sensitive", "an", "object"],
"sensitive text",
{"text": ["sensitive", "text"]},
{"text": "sensitive", "words": None},
{"text": "sensitive", "words": 7},
{"text": "sensitive", "words": "not a list"},
{"text": "sensitive", "words": ["not an object"]},
{"text": "sensitive", "words": [{"type": "word", "text": "ok"}, None]},
],
)
def test_transform_audio_transcription_response_wraps_malformed_payloads_with_the_raw_body(payload: object):
def test_transform_audio_transcription_response_keeps_the_body_out_of_malformed_payload_errors(payload: object):
raw_response = httpx.Response(200, json=payload)
with pytest.raises(ValueError, match="Error transforming ElevenLabs response: ") as exc_info:
ElevenLabsAudioTranscriptionConfig().transform_audio_transcription_response(raw_response=raw_response)
assert str(exc_info.value).endswith(f"\nResponse: {raw_response.text}")
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="Error transforming ElevenLabs response: ") as exc_info:
ElevenLabsAudioTranscriptionConfig().transform_audio_transcription_response(raw_response=raw_response)
failing_lookup: Final = exc_info.value.__context__
assert isinstance(failing_lookup, ValidationError)
assert str(exc_info.value) == f"Error transforming ElevenLabs response: {failing_lookup}"
assert "sensitive" not in "".join(traceback.format_exception(exc_info.value))
class TestElevenLabsAudioTranscription:

View file

@ -1,8 +1,10 @@
import base64
import json
import traceback
import httpx
import pytest
import respx
import litellm
from litellm.llms.gemini.audio_transcription.transformation import (
@ -241,6 +243,21 @@ class TestTransformResponse:
with pytest.raises(GeminiError, match="non-JSON"):
config.transform_audio_transcription_response(raw)
def test_non_completed_status_error_omits_response_body(self, config):
payload = json.loads(json.dumps(COMPLETED_RESPONSE))
payload["status"] = "incomplete"
payload["steps"][0]["content"][0]["text"] = "sensitive transcript text"
with pytest.raises(GeminiError) as excinfo:
config.transform_audio_transcription_response(make_response(payload))
assert "status=incomplete" in str(excinfo.value)
assert "sensitive transcript text" not in str(excinfo.value)
def test_non_json_error_omits_response_body(self, config):
raw = httpx.Response(200, text="sensitive body", request=httpx.Request("POST", "https://example.test"))
with pytest.raises(GeminiError) as excinfo:
config.transform_audio_transcription_response(raw)
assert "sensitive body" not in str(excinfo.value)
def test_word_without_offsets_survives(self, config):
payload = json.loads(json.dumps(COMPLETED_RESPONSE))
payload["steps"][0]["content"][0]["annotations"] = [{"type": "word_info", "text": "Hello"}]
@ -249,6 +266,66 @@ 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_retry_recovers_an_incomplete_interaction(self, router):
route = respx.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
side_effect=[
httpx.Response(200, json=self._interaction("incomplete")),
httpx.Response(200, json=COMPLETED_RESPONSE),
]
)
response = await router.atranscription(model="transcribe", file=("call.wav", AUDIO_BYTES, "audio/wav"))
assert route.call_count == 2
assert response.text == COMPLETED_RESPONSE["steps"][0]["content"][0]["text"]
@pytest.mark.asyncio
@respx.mock
@pytest.mark.parametrize("status", ["incomplete", "failed", "cancelled", "in_progress", None])
async def test_non_completed_interaction_retries_then_fails_without_transcript(self, router, status):
route = respx.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(200, json=self._interaction(status))
)
with pytest.raises(litellm.APIConnectionError) as excinfo:
await router.atranscription(model="transcribe", file=("call.wav", AUDIO_BYTES, "audio/wav"))
assert route.call_count == 3
assert f"status={status}" in str(excinfo.value)
assert "sensitive transcript text" not in str(excinfo.value)
@pytest.mark.asyncio
@respx.mock
async def test_response_field_of_unexpected_type_keeps_transcript_out_of_the_error(self, router):
payload = self._interaction("incomplete")
payload["steps"][0]["content"] = "sensitive transcript text"
respx.post("https://generativelanguage.googleapis.com/v1beta/interactions").mock(
return_value=httpx.Response(200, json=payload)
)
with pytest.raises(litellm.APIConnectionError) as excinfo:
await router.atranscription(model="transcribe", file=("call.wav", AUDIO_BYTES, "audio/wav"))
assert "sensitive transcript text" not in "".join(traceback.format_exception(excinfo.value))
class TestSubtitleSynthesisThroughHandler:
def _transform(self, config, response_format):
from unittest.mock import Mock

View file

@ -3,12 +3,15 @@ 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,
)
from litellm.llms.mistral.audio_transcription.transformation import (
MistralAudioTranscriptionConfig,
MistralAudioTranscriptionException,
)
from litellm.types.utils import TranscriptionResponse
from litellm.utils import ProviderConfigManager
@ -191,3 +194,22 @@ def test_mistral_audio_transcription_response_transform_empty():
assert isinstance(response, TranscriptionResponse)
assert response.text == ""
def test_mistral_audio_transcription_response_transform_keeps_non_json_body_out_of_the_error():
raw_response = httpx.Response(200, text='{"text": "sensitive')
with pytest.raises(MistralAudioTranscriptionException, match="non-JSON") as exc_info:
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)

View file

@ -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))

View file

@ -3,6 +3,7 @@ import pytest
from pydantic import ValidationError
from litellm.llms.ovhcloud.audio_transcription.transformation import OVHCloudAudioTranscriptionConfig
from litellm.llms.ovhcloud.utils import OVHCloudException
from litellm.types.utils import TranscriptionResponse
@ -98,3 +99,12 @@ def test_transform_audio_transcription_response_rejects_non_object_bodies(payloa
_transform(payload)
assert "spoken secret" not in str(exc_info.value)
def test_transform_audio_transcription_response_keeps_a_non_json_body_out_of_the_error():
raw_response = httpx.Response(200, text='{"text": "spoken secret')
with pytest.raises(OVHCloudException, match="non-JSON") as exc_info:
OVHCloudAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
assert "spoken secret" not in str(exc_info.value)

View file

@ -204,6 +204,17 @@ def test_scaleway_transform_response_raises_typed_exception_on_non_json():
)
def test_scaleway_transform_response_keeps_non_json_body_out_of_the_error():
raw_response = httpx.Response(
200, text='{"text": "sensitive', headers={"content-type": "application/json"}
)
with pytest.raises(ScalewayAudioTranscriptionException, match="non-JSON") as exc_info:
ScalewayAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
assert "sensitive" not in str(exc_info.value)
def test_scaleway_transform_response_returns_plain_text_for_non_json_content_type():
"""When Scaleway responds with text/srt/vtt (response_format="text" etc.),
the content-type is not application/json — return the body as plain text

View file

@ -5,7 +5,7 @@ from urllib.parse import urlparse
import httpx
import pytest
from pydantic import ValidationError
import litellm
from litellm.llms.vertex_ai.audio_transcription.transformation import (
@ -208,6 +208,18 @@ 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_field_of_unexpected_type_omits_response_body(self, config):
raw_response = httpx.Response(status_code=200, json={"results": "sensitive transcript"})
with pytest.raises(ValidationError) 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,

View file

@ -4,6 +4,7 @@ import os
import httpx
import pytest
from pydantic import ValidationError
import litellm
from litellm.llms.vertex_ai.audio_transcription.gemini_transcribe_transformation import (
@ -255,6 +256,20 @@ 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)
def test_field_of_unexpected_type_omits_response_body(self, config):
raw_response = httpx.Response(
status_code=200, json={"candidates": [{"content": {"parts": "sensitive transcript"}}]}
)
with pytest.raises(ValidationError) as excinfo:
config.transform_audio_transcription_response(raw_response)
assert "sensitive" not in str(excinfo.value)
class TestValidateEnvironment:
def test_sets_oauth_headers(self):

View file

@ -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])
@ -141,3 +146,12 @@ def test_transform_audio_transcription_response_rejects_non_object_bodies_withou
_transform_transcription_response(payload)
assert "input_value" not in str(exc_info.value)
def test_transform_audio_transcription_response_keeps_a_non_json_body_out_of_the_error() -> None:
raw_response = httpx.Response(200, text='{"text": "sensitive')
with pytest.raises(ValueError, match="Error transforming response to json") as exc_info:
IBMWatsonXAudioTranscriptionConfig().transform_audio_transcription_response(raw_response)
assert "sensitive" not in str(exc_info.value)

View file

@ -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)