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
- Inspect the error's finish_reason field: if it's "length", raise max_tokens / reduce prompt size or expected output length.
- If it's "content_filter", adjust the prompt to avoid policy triggers or relax the finish_reason_filter for that reason.
- If your use case tolerates truncation, configure allowed_finish_reasons in the BAML client to include the observed reason.
- 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
- Set max_tokens generously relative to expected output length to avoid finish_reason=length.
- Explicitly configure allowed finish reasons in the BAML client options to match your provider's behavior (e.g. tool_calls).
- Monitor finish_reason metadata in production to catch truncation trends early.
- Use retry/fallback orchestration strategies so a rejected finish reason doesn't fail the whole request.
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
- BamlError: BamlClientError: BamlClientFinishReasonError
- Finish reason not allowed
- AbortError
- BamlAbortError: Operation was aborted
- BamlError: BamlClientError: BamlClientHttpError
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)