fix: send whisper timestamp_granularities as bracketed array field

OpenAI's multipart transcription API expects array params as repeated
bracketed fields (timestamp_granularities[]=segment &
timestamp_granularities[]=word). LiteLLM stored the list under the bare
timestamp_granularities key, so OpenAI kept only the last value and a
combined ["segment", "word"] request silently returned just one
granularity (words OR segments, never both).

Rename the key to timestamp_granularities[] in
transform_audio_transcription_request so httpx encodes it as the repeated
bracketed fields OpenAI expects. This matches how the official
openai-python client serializes array form fields (array_format="brackets").

Fixes the multi-value case; single-value arrays already worked and
continue to.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
This commit is contained in:
richboyneedcash 2026-08-06 11:37:55 +08:00
parent ead62528e6
commit 7ec44c042a
2 changed files with 60 additions and 0 deletions

View file

@ -109,6 +109,16 @@ class OpenAIWhisperAudioTranscriptionConfig(BaseAudioTranscriptionConfig):
if "response_format" not in data:
data["response_format"] = "verbose_json" # ensures 'duration' is received - used for cost calculation
# OpenAI's multipart form API expects array params as repeated
# bracketed fields (`timestamp_granularities[]=segment` +
# `timestamp_granularities[]=word`). Sending the list under the bare
# `timestamp_granularities` key makes OpenAI keep only the last value,
# so a combined `["segment", "word"]` request silently returns just one
# granularity. Rename the key so the list is encoded as `[]` fields.
granularities = data.pop("timestamp_granularities", None)
if granularities is not None:
data["timestamp_granularities[]"] = granularities
return AudioTranscriptionRequestData(
data=data,
)

View file

@ -48,6 +48,56 @@ class TestWhisperTransformRequestResponseFormat:
assert data["response_format"] == "verbose_json"
class TestWhisperTransformTimestampGranularities:
"""
OpenAI's multipart form API expects array params as repeated bracketed
fields (``timestamp_granularities[]``). If the list is sent under the bare
``timestamp_granularities`` key, OpenAI keeps only the last value, so a
combined ``["segment", "word"]`` request silently returns one granularity.
"""
def _transform(self, optional_params: dict) -> dict:
config = OpenAIWhisperAudioTranscriptionConfig()
audio_file = io.BytesIO(b"fake audio")
audio_file.name = "test.wav"
result = config.transform_audio_transcription_request(
model="whisper-1",
audio_file=audio_file,
optional_params=optional_params,
litellm_params={},
)
return result.data
def test_multi_value_list_uses_bracketed_key(self):
"""A multi-value list is sent under the bracketed key so both are honored."""
data = self._transform(
{
"response_format": "verbose_json",
"timestamp_granularities": ["segment", "word"],
}
)
assert data["timestamp_granularities[]"] == ["segment", "word"]
# The bare key must not be present, otherwise OpenAI drops all but the last.
assert "timestamp_granularities" not in data
def test_single_value_list_uses_bracketed_key(self):
"""A single-value list is also sent under the bracketed key for consistency."""
data = self._transform(
{
"response_format": "verbose_json",
"timestamp_granularities": ["word"],
}
)
assert data["timestamp_granularities[]"] == ["word"]
assert "timestamp_granularities" not in data
def test_absent_granularities_not_added(self):
"""When timestamp_granularities is not provided, no bracketed key is added."""
data = self._transform({"response_format": "verbose_json"})
assert "timestamp_granularities[]" not in data
assert "timestamp_granularities" not in data
class TestWhisperTransformResponse:
def _make_response(self, *, text: str, content_type: str, is_json: bool):
mock = MagicMock()