finishReasonRetryClass function

FinishReasonClass? finishReasonRetryClass(
  1. AssistantMessage message
)

The structured retry verdict the wire finish_reason carries, or null when the failure has no finish_reason (HTTP-level errors, socket cuts, truncation) and the message-text nets decide instead. TERMINAL vetoes every retry. Budget/spending exhaustion (issue #926) is TERMINAL regardless of the wire form.

Implementation

FinishReasonClass? finishReasonRetryClass(AssistantMessage message) {
  if (message.stopReason != StopReason.error) return null;
  if (isBudgetExhaustion(message)) return FinishReasonClass.terminal;
  // Scoped to the openai-completions finish_reason family: its error
  // wording is the marker that the wire word came through the vocabulary
  // this table catalogues. Other adapters set rawStopReason too (Google
  // SAFETY/RECITATION, Anthropic sensitive) - refused content keeps the
  // terminal behavior it had before this table existed; replaying it is
  // a safety bug.
  final text = message.errorMessage;
  if (text == null || !text.startsWith('Provider finish_reason: ')) {
    return null;
  }
  final raw = message.rawStopReason;
  if (raw == null || raw.isEmpty) return null;
  return classifyFinishReason(raw);
}