{"record":{"id":"cc7cbefba81c0b0c","repo":"HKUDS/DeepTutor","slug":"api-returned-no-choices-in-response","errorCode":null,"errorMessage":"API returned no choices in response","messagePattern":"API returned no choices in response","errorType":"error_code","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"deeptutor/services/llm/providers/open_ai.py","lineNumber":100,"sourceCode":"            or kwargs.pop(\"max_completion_tokens\", None)\n            or getattr(self.config, \"max_tokens\", 4096)\n        )\n        if isinstance(requested_max_tokens, (int, float, str)):\n            max_tokens = int(requested_max_tokens)\n        else:\n            max_tokens = int(getattr(self.config, \"max_tokens\", 4096))\n        kwargs.update(get_token_limit_kwargs(model, max_tokens))\n\n        async def _call_api() -> TutorResponse:\n            request_kwargs: dict[str, object] = dict(kwargs)\n            response = await self.client.chat.completions.create(  # type: ignore[call-overload]\n                model=model,\n                messages=[{\"role\": \"user\", \"content\": prompt}],\n                **request_kwargs,\n            )\n\n            if not response.choices:\n                raise ValueError(\"API returned no choices in response\")\n            choice = response.choices[0]\n            message = choice.message\n            content = message.content or \"\"\n            finish_reason = choice.finish_reason\n            usage = response.usage.model_dump() if response.usage else {}\n            raw_response = response.model_dump() if hasattr(response, \"model_dump\") else {}\n            provider_label = (\n                \"azure\" if isinstance(self.client, openai.AsyncAzureOpenAI) else \"openai\"\n            )\n\n            return TutorResponse(\n                content=content,\n                raw_response=raw_response,\n                usage=usage,\n                provider=provider_label,\n                model=model,\n                finish_reason=finish_reason,\n                cost_estimate=self.calculate_cost(usage),","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/HKUDS/DeepTutor/blob/3e82f130422a813cdd73c10b21a44e9325f5821a/deeptutor/services/llm/providers/open_ai.py#L82-L118","documentation":"The OpenAI-compatible chat completions endpoint returned a 200 response whose `choices` array is empty, so the provider cannot extract any message. This usually indicates a server-side or gateway quirk (proxies, load balancers, content filters) rather than a malformed request. The provider defensively raises ValueError because downstream code assumes at least one choice exists.","triggerScenarios":"Calling provider.complete() against an OpenAI-compatible endpoint (proxy, LiteLLM, vLLM, Azure gateway) that occasionally returns `{\"choices\": []}`; content-filtered or truncated responses; misconfigured base_url pointing at a non-chat-completions route.","commonSituations":"Self-hosted OpenAI-compatible servers with buggy streaming/non-streaming parity, API gateways that strip choices on policy rejection, rate-limiter middlewares that return empty bodies, or model names the backend doesn't recognize but fails softly on.","solutions":["Retry the request — transient empty-choices responses usually succeed on the next call (execute_with_retry may need a wrapper that also retries this ValueError).","Inspect raw_response / server logs to confirm whether a content filter or policy removed the choice.","Verify base_url and model name are correct for the endpoint you're hitting.","If using a proxy/gateway, upgrade or configure it to return proper error statuses instead of empty choices."],"exampleFix":"# before\nresponse = await self.client.chat.completions.create(...)\nchoice = response.choices[0]\n\n# after\nif not response.choices:\n    raise ValueError(\"API returned no choices in response\")\nchoice = response.choices[0]","handlingStrategy":"retry","validationCode":"# before calling:\nif not provider.config.model:\n    raise LLMConfigError(\"configure model first\")  # unrelated, but cheap sanity check","typeGuard":null,"tryCatchPattern":"try:\n    resp = await provider.complete(prompt)\nexcept ValueError as e:\n    if \"no choices\" in str(e):\n        resp = await provider.complete(prompt)  # or backoff-retry loop\n    else:\n        raise","preventionTips":["Wrap complete() in a bounded retry with exponential backoff.","Log raw_response on failure to distinguish filter rejections from server glitches.","Avoid flaky OpenAI-compatible proxies; pin a stable gateway version."],"tags":["openai","chat-completions","empty-response","api"],"backgroundTag":"empty-choices-in-response","analyzedSha":"3e82f130422a813cdd73c10b21a44e9325f5821a","analyzedAt":"2026-08-27T06:57:25.364Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}