sipeed/picoclaw · error
no choices in response
Error message
no choices in response
What it means
The provider returned HTTP 200 with a syntactically valid JSON body whose choices array is empty (llm_client.go:183). Unlike transport or parse failures, the API worked but produced zero completions — typically content filtering, an empty generation, or a provider quirk. The client treats it as terminal rather than retrying.
Source
Thrown at cmd/membench/llm_client.go:183
continue // rate limit or server error → retry
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("API error %d: %s", resp.StatusCode, string(respBody))
}
lastErr = nil
break
}
if lastErr != nil {
return "", fmt.Errorf("after %d retries: %w", c.MaxRetries, lastErr)
}
var chatResp chatResponse
if err := json.Unmarshal(respBody, &chatResp); err != nil {
return "", fmt.Errorf("parse response: %w", err)
}
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}
content := strings.TrimSpace(chatResp.Choices[0].Message.Content)
// Strip any residual <think>...</think> blocks
if idx := strings.Index(content, "</think>"); idx >= 0 {
content = strings.TrimSpace(content[idx+len("</think>"):])
}
// Fallback: GLM/DeepSeek put thinking output in reasoning_content when thinking is enabled
if content == "" && chatResp.Choices[0].Message.ReasoningContent != "" {
content = strings.TrimSpace(chatResp.Choices[0].Message.ReasoningContent)
}
if content == "" {
return "", fmt.Errorf("empty LLM response")
}
return content, nil
}
View on GitHub (pinned to 49183d7e8d)
Solutions
- Log the full response body — finish_reason/usage usually reveals filtering or truncation
- Retry the single request: empty choices is often transient on self-hosted backends
- Raise max_tokens and check the model ID supports your content type
- If filtering is persistent, switch model or sanitize the prompt
Example fix
// before
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no choices in response")
}
// after
if len(chatResp.Choices) == 0 {
return "", fmt.Errorf("no choices in response (id=%s, usage=%+v, body=%s)", chatResp.ID, chatResp.Usage, truncate(respBody, 512))
} Defensive patterns
Strategy: retry
Validate before calling
// cheap preflight for content-filter-prone models: send a 1-token probe
if out, err := llm.Complete(ctx, "ping"); err != nil || out == "" {
return fmt.Errorf("model %s not producing completions (probe failed: %v)", model, err)
} Try / catch
if len(chatResp.Choices) == 0 {
if retried < 2 && resp.StatusCode == 200 {
log.Printf("empty choices (usage=%+v); retrying once", chatResp.Usage)
time.Sleep(time.Second)
goto retry // or restructure as a loop
}
return "", fmt.Errorf("no choices in response: body=%s", truncate(respBody, 256))
} Prevention
- Log the full body when choices is empty — finish_reason reveals filtering vs. truncation
- Set max_tokens > 0 and verify the model ID exists before the run
- For reasoning models, check whether output landed in a field the client ignores
- Retry empty-choices once or twice; on self-hosted backends it is frequently transient
When it happens
Trigger: Content-policy filter dropping the only candidate; provider returning {"choices":[]} on overloaded internal routing; empty completion after max_tokens=0 or a misconfigured sampling payload; some proxies stripping choices on transform.
Common situations: Benchmark prompts with adversarial or sensitive LOCOMO content tripping filters; reasoning models returning all output in a field the client ignores; near-zero max_tokens; flaky third-party gateways.
Related errors
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/9d91939cfc8b50db.
Report an issue: GitHub.