BerriAI/litellm · error · ValueError

Invalid response from transcription provider, expected Trans

Error message

Invalid response from transcription provider, expected TranscriptionResponse, but got {type(response)}

What it means

After awaiting the transcription call (covering cached dicts, direct TranscriptionResponse objects, and coroutines), litellm validates that the final result is a TranscriptionResponse. Anything else — str, a dict that failed coercion, None, or a custom object — triggers this ValueError, meaning the handler violated the return contract.

Source

Thrown at litellm/main.py:7576

        # Add the context to the function
        ctx: Final = contextvars.copy_context()
        func_with_context: Final = partial(ctx.run, func)

        _, custom_llm_provider, _, _ = get_llm_provider(model=model, api_base=kwargs.get("api_base", None))

        # Await normally
        init_response: Final = await loop.run_in_executor(None, func_with_context)
        if isinstance(init_response, dict):
            response = _transcription_response_from_cached_dict(init_response)
        elif isinstance(init_response, TranscriptionResponse):  ## CACHING SCENARIO
            response = init_response
        elif asyncio.iscoroutine(init_response):
            response = await init_response
        else:
            # Call the synchronous function using run_in_executor
            response = await loop.run_in_executor(None, func_with_context)
        if not isinstance(response, TranscriptionResponse):
            raise ValueError(
                f"Invalid response from transcription provider, expected TranscriptionResponse, but got {type(response)}"
            )

        # Store duration in _hidden_params for cost calculation without
        # exposing it in the response body. Adding duration to the response
        # tricks the OpenAI SDK's "best match deserialization" into thinking
        # a plain Transcription is a TranscriptionVerbose/Diarized type.
        if response is not None and not isinstance(response, Coroutine) and file is not None:
            existing_duration: Final = getattr(response, "duration", None)
            if existing_duration is None:
                calculated_duration: Final = calculate_request_duration(file)
                if calculated_duration is not None:
                    response._hidden_params["audio_transcription_duration"] = calculated_duration

        return response
    except Exception as e:
        custom_llm_provider = custom_llm_provider or "openai"
        raise exception_type(

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Return litellm.TranscriptionResponse(text=..., ...) from custom transcription handlers
  2. If a logger/hook short-circuits the call, return a proper TranscriptionResponse per the current API
  3. Reproduce with litellm.set_verbose = True and print type(response) in the handler
  4. Upgrade litellm if your handler follows the latest docs

Example fix

# before
class MySTT(CustomLLM):
    def transcription(self, **kwargs):
        return "hello world"  # raw string -> ValueError

# after
class MySTT(CustomLLM):
    def transcription(self, **kwargs):
        return TranscriptionResponse(text="hello world")
Defensive patterns

Strategy: type-guard

Validate before calling

# For custom transcription handlers, wrap before returning
from litellm.types.utils import TranscriptionResponse

def to_transcription_response(raw) -> TranscriptionResponse:
    if isinstance(raw, TranscriptionResponse):
        return raw
    if isinstance(raw, dict):
        return TranscriptionResponse(**raw)
    return TranscriptionResponse(text=str(raw))

Type guard

from litellm.types.utils import TranscriptionResponse

def is_transcription_response(resp: object) -> bool:
    return isinstance(resp, TranscriptionResponse)

Try / catch

try:
    resp = litellm.transcription(model=model, file=f)
except ValueError as e:
    if "expected TranscriptionResponse" in str(e):
        # custom handler/cache broke the return contract
        raise
    raise

Prevention

When it happens

Trigger: A CustomLLM transcription handler returning a raw string/dict instead of TranscriptionResponse; a logging hook or cache returning an incompatible object; a mocked/monkeypatched transcription function in tests.

Common situations: Writing a custom transcription provider and forgetting to wrap the result; a caching layer returning serialized JSON that no longer coerces; litellm version drift changing the expected type.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/13b7461c58ff8a4d. Report an issue: GitHub.