BoundaryML/baml · error · ExposedError::FinishReasonError

Finish reason not allowed

Error message

Finish reason not allowed

What it means

BAML exposes a finish_reason_filter on each LLM client node. After a successful LLM response, the orchestrator checks whether the model's finish_reason (e.g. stop, length, content_filter, tool_calls) is allowed. If not, it raises a FinishReasonError with the prompt, raw output, and the offending finish reason — the call technically succeeded but the result is likely truncated or otherwise unusable.

Solutions

  1. Inspect the error's finish_reason field: if it's "length", raise max_tokens / reduce prompt size or expected output length.
  2. If it's "content_filter", adjust the prompt to avoid policy triggers or relax the finish_reason_filter for that reason.
  3. If your use case tolerates truncation, configure allowed_finish_reasons in the BAML client to include the observed reason.
  4. Add a retry strategy with a fallback client so rejected finish reasons trigger another attempt instead of failing the request.

Example fix

// before
generator Gpt4 {
  provider bcp/openai
  options { model gpt-4o max_tokens 50 }
}
// after
generator Gpt4 {
  provider bcp/openai
  options { model gpt-4o max_tokens 2048 }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: estimate output size is within max_tokens budget
fn fits_budget(prompt_tokens: usize, expected_output_tokens: usize, max_tokens: usize) -> bool {
    prompt_tokens + expected_output_tokens <= max_tokens
}

Type guard

fn finish_reason_ok(resp: &LlmSuccessResponse, allowed: &[&str]) -> bool {
    resp.metadata.finish_reason
        .as_deref().map(|fr| allowed.contains(&fr)).unwrap_or(false)
}

Try / catch

match result {
    Err(e) if e.to_string().contains("Finish reason not allowed") => {
        warn!("finish reason rejected: {e}");
        retry_with_larger_budget_or_fallback()?
    }
    other => other,
}

Prevention

When it happens

Trigger: orchestrate() receives LLMResponse::Success whose metadata.finish_reason is rejected by node.finish_reason_filter().is_allowed() — typically finish_reason == "length" (max_tokens hit) or "content_filter" when the filter only allows "stop".

Common situations: Model output truncated because max_tokens is too small; content filtered by provider policy; tool-call finish reasons when the client isn't configured for tools; strict finish-reason filters configured in BAML client strategy options.

Related errors


AI-assisted analysis of BoundaryML/baml@bd85ce9dee (2026-09-12). Data as JSON: /api/errors/54476e2002f85e1b. Report an issue: GitHub.

Appendix: source

Thrown at engine/baml-runtime/src/internal/llm_client/orchestrator/call.rs:114

                    Err(e) => {
                        return Some((
                            node.scope,
                            LLMResponse::InternalFailure(e.to_string()),
                            Some(Err(anyhow::anyhow!(e.to_string()))),
                        ));
                    }
                };

                let ctx = CtxWithHttpRequestId::from(ctx);
                let response = node.single_call(&ctx, &prompt).await;
                let parsed_response = match &response {
                    LLMResponse::Success(s) => {
                        if !node
                            .finish_reason_filter()
                            .is_allowed(s.metadata.finish_reason.as_ref())
                        {
                            let message = "Finish reason not allowed".to_string();
                            Some(Err(anyhow::anyhow!(
                                crate::errors::ExposedError::FinishReasonError {
                                    prompt: prompt.to_string(),
                                    raw_output: s.content.clone(),
                                    detailed_message: message.clone(),
                                    message,
                                    finish_reason: s.metadata.finish_reason.clone(),
                                }
                            )))
                        } else {
                            Some(parse_fn(&s.content))
                        }
                    }
                    LLMResponse::LLMFailure(LLMErrorResponse {
                        code,
                        client,
                        message,
                        raw_response,
                        ..

View on GitHub (pinned to bd85ce9dee)