fix(elevenlabs): add prompt support and fix array param serialization in transcription (#25131)

- Add `prompt` to supported OpenAI params for ElevenLabs transcription (#25065)
- Fix `str(value)` serialization that mangled list/dict params like `keyterms` into
  Python repr strings instead of valid JSON (#25066)
- Add `_serialize_form_value()` helper: bool→lowercase, list/dict→JSON, else str()

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kiyeonjeon21 2026-04-08 13:04:38 +09:00 committed by GitHub
parent ab4b379c1a
commit 51c9553920
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 169 additions and 4 deletions

View file

@ -2,6 +2,7 @@
Translates from OpenAI's `/v1/audio/transcriptions` to ElevenLabs's `/v1/speech-to-text`
"""
import json
from typing import List, Optional, Union
from httpx import Headers, Response
@ -31,7 +32,7 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
def get_supported_openai_params(
self, model: str
) -> List[OpenAIAudioTranscriptionOptionalParams]:
return ["language", "temperature"]
return ["language", "prompt", "temperature"]
def map_openai_params(
self,
@ -46,6 +47,10 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
if k == "language":
# Map OpenAI language format to ElevenLabs language_code
optional_params["language_code"] = v
elif k == "prompt":
# Pass prompt through — ElevenLabs Scribe accepts it
# as vocabulary context
optional_params["prompt"] = v
else:
optional_params[k] = v
return optional_params
@ -57,6 +62,20 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
message=error_message, status_code=status_code, headers=headers
)
@staticmethod
def _serialize_form_value(value) -> str:
"""Serialize a value for multipart form data.
- bool lowercase "true"/"false"
- list/dict JSON string
- other str()
"""
if isinstance(value, bool):
return str(value).lower()
if isinstance(value, (list, dict)):
return json.dumps(value)
return str(value)
def transform_audio_transcription_request(
self,
model: str,
@ -84,8 +103,7 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
#########################################################
for key, value in optional_params.items():
if key in self.get_supported_openai_params(model) and value is not None:
# Convert values to strings for form data, but skip None values
form_data[key] = str(value)
form_data[key] = self._serialize_form_value(value)
#########################################################
# Add Provider Specific Parameters
@ -97,7 +115,7 @@ class ElevenLabsAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
)
for key, value in provider_specific_params.items():
form_data[key] = str(value)
form_data[key] = self._serialize_form_value(value)
#########################################################
#########################################################

View file

@ -0,0 +1,147 @@
import json
import pytest
import litellm
from litellm.llms.base_llm.audio_transcription.transformation import (
BaseAudioTranscriptionConfig,
)
from litellm.llms.elevenlabs.audio_transcription.transformation import (
ElevenLabsAudioTranscriptionConfig,
)
from litellm.utils import ProviderConfigManager
@pytest.fixture()
def config():
return ElevenLabsAudioTranscriptionConfig()
def test_elevenlabs_config_registered():
"""Ensure ElevenLabs audio transcription config is registered."""
config = ProviderConfigManager.get_provider_audio_transcription_config(
model="elevenlabs/scribe_v2",
provider=litellm.LlmProviders.ELEVENLABS,
)
assert config is not None
assert isinstance(config, BaseAudioTranscriptionConfig)
assert isinstance(config, ElevenLabsAudioTranscriptionConfig)
def test_supported_openai_params_includes_prompt(config):
"""Fixes #25065 — prompt must be in supported params."""
params = config.get_supported_openai_params("scribe_v2")
assert "prompt" in params
assert "language" in params
assert "temperature" in params
def test_map_openai_params_prompt(config):
"""prompt should be mapped through to optional_params."""
result = config.map_openai_params(
non_default_params={"prompt": "technical terms: LiteLLM, Scribe"},
optional_params={},
model="scribe_v2",
drop_params=False,
)
assert result["prompt"] == "technical terms: LiteLLM, Scribe"
def test_map_openai_params_language_mapped_to_language_code(config):
"""language should be mapped to language_code for ElevenLabs."""
result = config.map_openai_params(
non_default_params={"language": "en"},
optional_params={},
model="scribe_v2",
drop_params=False,
)
assert result["language_code"] == "en"
assert "language" not in result
def test_serialize_form_value_bool(config):
"""Booleans should be lowercase strings."""
assert config._serialize_form_value(True) == "true"
assert config._serialize_form_value(False) == "false"
def test_serialize_form_value_list(config):
"""Fixes #25066 — lists must be JSON-serialized, not Python repr."""
value = ["term1", "term2"]
result = config._serialize_form_value(value)
assert result == '["term1", "term2"]'
# Verify it's valid JSON
assert json.loads(result) == ["term1", "term2"]
def test_serialize_form_value_dict(config):
"""Dicts must be JSON-serialized."""
value = {"key": "val"}
result = config._serialize_form_value(value)
assert json.loads(result) == {"key": "val"}
def test_serialize_form_value_scalar(config):
"""Scalars should use str()."""
assert config._serialize_form_value(0.5) == "0.5"
assert config._serialize_form_value(42) == "42"
assert config._serialize_form_value("hello") == "hello"
def test_transform_request_with_prompt(config):
"""prompt param should appear in form data after transform."""
result = config.transform_audio_transcription_request(
model="scribe_v2",
audio_file=b"fake audio bytes",
optional_params={"prompt": "vocabulary hint", "temperature": 0.5},
litellm_params={},
)
assert result.data["prompt"] == "vocabulary hint"
assert result.data["temperature"] == "0.5"
assert result.data["model_id"] == "scribe_v2"
def test_transform_request_array_provider_param(config):
"""Provider-specific array params should be JSON-encoded, not Python repr."""
result = config.transform_audio_transcription_request(
model="scribe_v2",
audio_file=b"fake audio bytes",
optional_params={"keyterms": ["Atlas", "KIT"]},
litellm_params={},
)
raw = result.data["keyterms"]
assert raw == '["Atlas", "KIT"]'
assert json.loads(raw) == ["Atlas", "KIT"]
def test_transform_request_bool_provider_param(config):
"""Provider-specific bool params should be lowercase strings."""
result = config.transform_audio_transcription_request(
model="scribe_v2",
audio_file=b"fake audio bytes",
optional_params={"diarize": True},
litellm_params={},
)
assert result.data["diarize"] == "true"
def test_get_complete_url(config):
url = config.get_complete_url(
api_base=None,
api_key="fake-key",
model="scribe_v2",
optional_params={},
litellm_params={},
)
assert url == "https://api.elevenlabs.io/v1/speech-to-text"
def test_get_complete_url_custom_base(config):
url = config.get_complete_url(
api_base="https://custom.api.example.com/",
api_key="fake-key",
model="scribe_v2",
optional_params={},
litellm_params={},
)
assert url == "https://custom.api.example.com/v1/speech-to-text"