Hmbown/CodeWhale · error

Model returned terminal stop reason

Error message

Model returned terminal stop reason `{reason}` with no answer or tool call.

What it means

The engine finished a model turn in which the assistant produced neither an answer message nor a tool call, but the provider supplied a terminal stop_reason. The turn loop logs the raw reason string and fails the turn with a classified error envelope so the user sees why nothing happened. It exists because silent empty completions otherwise end a turn as 'Completed' with no output.

Solutions

  1. Read the stop reason shown in the log and address its cause (raise max_tokens for 'length', adjust prompt for 'refusal'/'content_filter').
  2. Retry the turn — some filters/refusals are transient or prompt-sensitive.
  3. Check the provider/model configuration (model name, safety settings, max output tokens) in the session config.
  4. If a proxy or gateway sits between the client and provider, verify it is not truncating or filtering responses.

Example fix

// before: turn fails on stop_reason "length" with empty output
// after: configure the session with a larger output budget so content arrives before the cutoff
max_output_tokens = 8192  // was 256, exhausted before any text streamed
Defensive patterns

Strategy: retry

Try / catch

// on turn error containing "terminal stop reason", read the reason from the message and decide
if msg.contains("stop reason `length`") { raise max_tokens; retry(); }
else if msg.contains("content_filter") || msg.contains("refusal") { rephrase prompt; retry(); }

Prevention

When it happens

Trigger: run_turn's stream-completion check: the stream closes cleanly, accumulated content is empty and no tool calls were emitted, while the provider's stop_reason is Some(reason) (e.g. 'content_filter', 'length', 'refusal'). Reached via handle_send_message.

Common situations: Provider-side content filters blocking the response; a max-tokens/length cutoff consuming the entire budget before any text was emitted; a model refusing or safety-terminating on a prompt; misconfigured model names behind filtering proxies.

Understand the failure class

Background: "empty response", "returned no data", "empty embeddings": what HTTP 200-with-empty-body errors mean across libraries — this error's family across 36 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/dd04ef43860a40e4. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/core/engine/turn_loop.rs:2606

                    let message = if has_provider_reasoning
                        && stop_reason_is_output_limit(stop_reason.as_deref())
                    {
                        format!(
                            "Model reached the response output limit with no answer or tool call (requested allowance: {} tokens, including reasoning).",
                            stream_request.max_tokens
                        )
                    } else if has_provider_reasoning {
                        let reason = codewhale_models::stop_reason_detail(stop_reason.as_deref());
                        format!(
                            "Model returned reasoning but no answer or tool call; the provider response was incomplete (stop reason: {}).",
                            reason
                                .chars()
                                .flat_map(char::escape_default)
                                .take(120)
                                .collect::<String>()
                        )
                    } else if let Some(reason) = stop_reason.as_deref() {
                        format!(
                            "Model returned terminal stop reason `{reason}` with no answer or tool call."
                        )
                    } else {
                        "Model stream ended with no answer or tool call.".to_string()
                    };
                    crate::logging::warn(&message);
                    turn_error = Some(message.clone());
                    let _ = self
                        .tx_event
                        .send(Event::error(ErrorEnvelope::classify(message, true)))
                        .await;
                }

                if turn_error.is_none() {
                    if !turn.budget_exhausted_final_report {
                        turn.stop_diagnostics.reason = Some(TurnStopReason::ProviderNoToolCall);
                    }
                    // This branch received no calls and dispatches no tools.

View on GitHub (pinned to 73e0f67d83)