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

  1. Log the full response body — finish_reason/usage usually reveals filtering or truncation
  2. Retry the single request: empty choices is often transient on self-hosted backends
  3. Raise max_tokens and check the model ID supports your content type
  4. 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

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.