Skip to content

Normalize / categorize LLM finish reasons across providers in ChatGenerationMetadata (keep raw reason) #5146

@nnsns

Description

@nnsns

Actual behavior

ChatGenerationMetadata.getFinishReason() exposes the provider-specific finish reason as a raw String.
In multi-provider setups the values differ (e.g. stop, end_turn, STOP, length, max_tokens, MAX_TOKENS, content_filter, SAFETY, etc.), so applications must implement their own mapping logic to build provider-agnostic audits/metrics/alerts. This leads to duplicated, brittle code and inconsistent semantics across projects.

Expected behavior

Spring AI should provide a normalized / categorized finish reason in addition to the raw provider value.

Example (additive, backward compatible):

enum FinishReasonCategory {
  COMPLETED,   // normal stop/end_turn/STOP
  TRUNCATED,   // length/max_tokens/MAX_TOKENS/context_window_exceeded
  TOOL_CALL,   // tool_calls/tool_use
  FILTERED,    // content_filter/SAFETY/refusal/SPII/PROHIBITED_CONTENT/RECITATION
  OTHER, UNKNOWN
}

interface ChatGenerationMetadata {
  String getFinishReason();                 // existing raw reason (unchanged)
  FinishReasonCategory getFinishCategory(); // new normalized category
}

Provider modules would map their native reasons to the category while preserving raw. Unknown/new values should map to UNKNOWN but keep the raw string intact.

This would make it much easier to:

  • colorize/flag audit logs consistently (green=COMPLETED, red=TRUNCATED/FILTERED, yellow=TOOL_CALL/UNKNOWN),
  • track truncations/safety blocks per model,
  • correlate structured output parse failures with truncation.

Steps to reproduce

  1. Configure two chat models/providers (e.g. OpenAI-compatible + Anthropic/Gemini).
  2. Call them via ChatClient and read Generation.getMetadata().getFinishReason().
  3. Observe that the finish reason strings are provider-specific and require custom mapping to interpret consistently (e.g. stop vs end_turn vs STOP; length vs max_tokens vs MAX_TOKENS; safety-related values vary widely).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions