{"record":{"id":"54476e2002f85e1b","repo":"BoundaryML/baml","slug":"finish-reason-not-allowed","errorCode":null,"errorMessage":"Finish reason not allowed","messagePattern":"Finish reason not allowed","errorType":"exception","errorClass":"ExposedError::FinishReasonError","httpStatus":null,"severity":"error","filePath":"engine/baml-runtime/src/internal/llm_client/orchestrator/call.rs","lineNumber":114,"sourceCode":"                    Err(e) => {\n                        return Some((\n                            node.scope,\n                            LLMResponse::InternalFailure(e.to_string()),\n                            Some(Err(anyhow::anyhow!(e.to_string()))),\n                        ));\n                    }\n                };\n\n                let ctx = CtxWithHttpRequestId::from(ctx);\n                let response = node.single_call(&ctx, &prompt).await;\n                let parsed_response = match &response {\n                    LLMResponse::Success(s) => {\n                        if !node\n                            .finish_reason_filter()\n                            .is_allowed(s.metadata.finish_reason.as_ref())\n                        {\n                            let message = \"Finish reason not allowed\".to_string();\n                            Some(Err(anyhow::anyhow!(\n                                crate::errors::ExposedError::FinishReasonError {\n                                    prompt: prompt.to_string(),\n                                    raw_output: s.content.clone(),\n                                    detailed_message: message.clone(),\n                                    message,\n                                    finish_reason: s.metadata.finish_reason.clone(),\n                                }\n                            )))\n                        } else {\n                            Some(parse_fn(&s.content))\n                        }\n                    }\n                    LLMResponse::LLMFailure(LLMErrorResponse {\n                        code,\n                        client,\n                        message,\n                        raw_response,\n                        ..","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/BoundaryML/baml/blob/bd85ce9dee1463ff04d27efd20531013a4ff46c1/engine/baml-runtime/src/internal/llm_client/orchestrator/call.rs#L96-L132","documentation":"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.","triggerScenarios":"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\".","commonSituations":"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.","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."],"exampleFix":"// before\ngenerator Gpt4 {\n  provider bcp/openai\n  options { model gpt-4o max_tokens 50 }\n}\n// after\ngenerator Gpt4 {\n  provider bcp/openai\n  options { model gpt-4o max_tokens 2048 }\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: estimate output size is within max_tokens budget\nfn fits_budget(prompt_tokens: usize, expected_output_tokens: usize, max_tokens: usize) -> bool {\n    prompt_tokens + expected_output_tokens <= max_tokens\n}","typeGuard":"fn finish_reason_ok(resp: &LlmSuccessResponse, allowed: &[&str]) -> bool {\n    resp.metadata.finish_reason\n        .as_deref().map(|fr| allowed.contains(&fr)).unwrap_or(false)\n}","tryCatchPattern":"match result {\n    Err(e) if e.to_string().contains(\"Finish reason not allowed\") => {\n        warn!(\"finish reason rejected: {e}\");\n        retry_with_larger_budget_or_fallback()?\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["llm","finish-reason","truncation","max-tokens"],"backgroundTag":"unexpected-response-shape","analyzedSha":"bd85ce9dee1463ff04d27efd20531013a4ff46c1","analyzedAt":"2026-09-12T03:38:25.718Z","contentChangedAt":"2026-09-12T03:38:25.718Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}