-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
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
- Configure two chat models/providers (e.g. OpenAI-compatible + Anthropic/Gemini).
- Call them via
ChatClientand readGeneration.getMetadata().getFinishReason(). - Observe that the finish reason strings are provider-specific and require custom mapping to interpret consistently (e.g.
stopvsend_turnvsSTOP;lengthvsmax_tokensvsMAX_TOKENS; safety-related values vary widely).