From 7dc382ecbbe3c829f27db302ef6c7febb3ed5e62 Mon Sep 17 00:00:00 2001 From: Maximilian Kohl Date: Wed, 5 Aug 2026 23:11:22 +0100 Subject: [PATCH] fix: preserve `logprobs` on audio transcription responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `convert_to_model_response_object` copied only `language/task/duration/words/segments` from a transcription response, so `logprobs` was silently dropped — even when the caller explicitly asked for it via `include[]=logprobs`. Effect: for `gpt-4o-transcribe` there was no way to read per-token confidence through the proxy. The only visible workaround was `stream=True`, which returns the raw SSE body as `text` with `usage: null` — and therefore silently disables cost tracking and budget enforcement for every transcription request. Adding `logprobs` to the copied keys makes the non-streaming path complete, so callers can have both confidence scores and correct spend accounting. Tests: two regression tests (with and without `logprobs`) in `tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py`. --- .../convert_dict_to_response.py | 2 +- .../test_convert_dict_to_response.py | 47 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py index a6f10e1ede3..a6819d187e8 100644 --- a/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py +++ b/litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py @@ -809,7 +809,7 @@ def convert_to_model_response_object( if "text" in response_object: model_response_object.text = response_object["text"] - optional_keys: Final = ["language", "task", "duration", "words", "segments"] + optional_keys: Final = ["language", "task", "duration", "words", "segments", "logprobs"] for key in optional_keys: # not guaranteed to be in response if key in response_object: setattr(model_response_object, key, response_object[key]) diff --git a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py b/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py index 293e5de304f..6c10b6af444 100644 --- a/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py +++ b/tests/test_litellm/litellm_core_utils/llm_response_utils/test_convert_dict_to_response.py @@ -14,6 +14,7 @@ from litellm.types.utils import ( ChatCompletionMessageToolCall, Function, ModelResponse, + TranscriptionResponse, ) OPENAI_CUSTOM_TOOL_CALL_RESPONSE = { @@ -102,3 +103,49 @@ def test_handle_invalid_parallel_tool_calls_skips_custom_tool_calls(): ) result = _handle_invalid_parallel_tool_calls([custom_tool_call, function_tool_call]) assert result == [custom_tool_call, function_tool_call] + + +def test_audio_transcription_preserves_logprobs(): + """`logprobs` must survive conversion (requested via `include[]=logprobs`). + + Regression: only language/task/duration/words/segments were copied, so + `logprobs` was silently dropped for `gpt-4o-transcribe` and callers had no + way to read the per-token confidence without falling back to `stream=True`. + """ + response_object = { + "text": "Guten Tag.", + "logprobs": [ + {"token": "Guten", "logprob": -0.01, "bytes": [71]}, + {"token": " Tag", "logprob": -0.02, "bytes": [32]}, + ], + "usage": { + "type": "tokens", + "input_tokens": 30, + "output_tokens": 4, + "total_tokens": 34, + "input_token_details": {"text_tokens": 0, "audio_tokens": 30}, + }, + } + + result = convert_to_model_response_object( + response_object=response_object, + model_response_object=TranscriptionResponse(), + response_type="audio_transcription", + ) + + assert result.text == "Guten Tag." + assert result.logprobs == response_object["logprobs"] + # usage must keep working alongside it (cost tracking depends on it) + assert result.usage.input_tokens == 30 + + +def test_audio_transcription_without_logprobs_is_unchanged(): + """No `logprobs` in the response → attribute simply absent, no crash.""" + result = convert_to_model_response_object( + response_object={"text": "Guten Tag."}, + model_response_object=TranscriptionResponse(), + response_type="audio_transcription", + ) + + assert result.text == "Guten Tag." + assert getattr(result, "logprobs", None) is None