{"record":{"id":"13b7461c58ff8a4d","repo":"BerriAI/litellm","slug":"invalid-response-from-transcription-provider-expe","errorCode":null,"errorMessage":"Invalid response from transcription provider, expected TranscriptionResponse, but got {type(response)}","messagePattern":"Invalid response from transcription provider, expected TranscriptionResponse, but got (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/main.py","lineNumber":7576,"sourceCode":"        # Add the context to the function\n        ctx: Final = contextvars.copy_context()\n        func_with_context: Final = partial(ctx.run, func)\n\n        _, custom_llm_provider, _, _ = get_llm_provider(model=model, api_base=kwargs.get(\"api_base\", None))\n\n        # Await normally\n        init_response: Final = await loop.run_in_executor(None, func_with_context)\n        if isinstance(init_response, dict):\n            response = _transcription_response_from_cached_dict(init_response)\n        elif isinstance(init_response, TranscriptionResponse):  ## CACHING SCENARIO\n            response = init_response\n        elif asyncio.iscoroutine(init_response):\n            response = await init_response\n        else:\n            # Call the synchronous function using run_in_executor\n            response = await loop.run_in_executor(None, func_with_context)\n        if not isinstance(response, TranscriptionResponse):\n            raise ValueError(\n                f\"Invalid response from transcription provider, expected TranscriptionResponse, but got {type(response)}\"\n            )\n\n        # Store duration in _hidden_params for cost calculation without\n        # exposing it in the response body. Adding duration to the response\n        # tricks the OpenAI SDK's \"best match deserialization\" into thinking\n        # a plain Transcription is a TranscriptionVerbose/Diarized type.\n        if response is not None and not isinstance(response, Coroutine) and file is not None:\n            existing_duration: Final = getattr(response, \"duration\", None)\n            if existing_duration is None:\n                calculated_duration: Final = calculate_request_duration(file)\n                if calculated_duration is not None:\n                    response._hidden_params[\"audio_transcription_duration\"] = calculated_duration\n\n        return response\n    except Exception as e:\n        custom_llm_provider = custom_llm_provider or \"openai\"\n        raise exception_type(","sourceCodeStart":7558,"sourceCodeEnd":7594,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/main.py#L7558-L7594","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Return litellm.TranscriptionResponse(text=..., ...) from custom transcription handlers","If a logger/hook short-circuits the call, return a proper TranscriptionResponse per the current API","Reproduce with litellm.set_verbose = True and print type(response) in the handler","Upgrade litellm if your handler follows the latest docs"],"exampleFix":"# before\nclass MySTT(CustomLLM):\n    def transcription(self, **kwargs):\n        return \"hello world\"  # raw string -> ValueError\n\n# after\nclass MySTT(CustomLLM):\n    def transcription(self, **kwargs):\n        return TranscriptionResponse(text=\"hello world\")","handlingStrategy":"type-guard","validationCode":"# For custom transcription handlers, wrap before returning\nfrom litellm.types.utils import TranscriptionResponse\n\ndef to_transcription_response(raw) -> TranscriptionResponse:\n    if isinstance(raw, TranscriptionResponse):\n        return raw\n    if isinstance(raw, dict):\n        return TranscriptionResponse(**raw)\n    return TranscriptionResponse(text=str(raw))","typeGuard":"from litellm.types.utils import TranscriptionResponse\n\ndef is_transcription_response(resp: object) -> bool:\n    return isinstance(resp, TranscriptionResponse)","tryCatchPattern":"try:\n    resp = litellm.transcription(model=model, file=f)\nexcept ValueError as e:\n    if \"expected TranscriptionResponse\" in str(e):\n        # custom handler/cache broke the return contract\n        raise\n    raise","preventionTips":["Always construct typed response objects in custom handlers instead of returning raw strings/dicts","Add unit tests asserting isinstance(handler.transcription(...), TranscriptionResponse)"],"tags":["litellm","transcription","response-type","custom-handler"],"backgroundTag":"unexpected-response-type","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}