/** * toolCallContinuation — Utilities for detecting and classifying LLM finish * reasons across providers. Used to detect max_tokens truncation and normalize * stop reason extraction from provider-specific metadata formats. * * @module */ import type { AIMessageChunk } from '@langchain/core/messages'; import { FinishReasons } from '@/common'; /** * Extract the finish/stop reason from an AIMessageChunk's response_metadata. * Centralizes provider-specific metadata access patterns. * * Provider formats: * - OpenAI/Azure: `response_metadata.finish_reason` * - Anthropic direct: `response_metadata.stop_reason` * - Bedrock invoke: `response_metadata.stopReason` * - Bedrock streaming: `response_metadata.messageStop.stopReason` * - VertexAI/Google: `response_metadata.finishReason` * * @param chunk - The final accumulated AIMessageChunk * @returns The finish reason string, or undefined if not found */ export function extractFinishReason( chunk: AIMessageChunk | undefined ): string | undefined { if (!chunk || !('response_metadata' in chunk)) return undefined; const meta = chunk.response_metadata as Record; if (!meta) return undefined; // Bedrock streaming nests stopReason inside messageStop: { stopReason: '...' } const messageStop = meta.messageStop as Record | undefined; return ( (meta.finish_reason as string | undefined) ?? // OpenAI/Azure (meta.stop_reason as string | undefined) ?? // Anthropic direct API (meta.stopReason as string | undefined) ?? // Bedrock invoke (non-streaming) (messageStop?.stopReason as string | undefined) ?? // Bedrock streaming (meta.finishReason as string | undefined) // VertexAI/Google ); } /** * Check if a finish reason indicates the response was truncated by token limits. * * @param reason - The finish reason from extractFinishReason() * @returns true if the response was cut short by max_tokens */ export function isMaxTokensFinish(reason: string | undefined): boolean { if (!reason) return false; return reason === FinishReasons.MAX_TOKENS || reason === FinishReasons.LENGTH; }