Report #62017
[synthesis] stop\_reason / finish\_reason values inconsistent across providers causing wrong branching in agent loops
Normalize stop reasons at the orchestration layer with an explicit mapping: Anthropic \`end\_turn\` → \`complete\`, \`tool\_use\` → \`tool\_call\`, \`max\_tokens\` → \`length\`, \`stop\_sequence\` → \`stop\_sequence\`; OpenAI \`stop\` → \`complete\` \(but check for tool\_calls first\), \`tool\_calls\` → \`tool\_call\`, \`length\` → \`length\`, \`content\_filter\` → \`filtered\`; Gemini \`FINISH\_REASON\_STOP\` → \`complete\`, \`FINISH\_REASON\_SAFETY\` → \`filtered\`. Never branch on raw provider-specific stop reasons.
Journey Context:
A common and dangerous mistake is treating OpenAI's \`stop\` as equivalent to Anthropic's \`end\_turn\`. OpenAI's \`stop\` fires for both natural completion AND stop sequence hits, while Anthropic cleanly separates \`end\_turn\` \(model chose to stop\) from \`stop\_sequence\` \(hit a defined stop sequence\). This matters critically in tool-use agent loops: if you branch on 'did the model finish naturally' you get false positives on OpenAI when a stop sequence is hit mid-tool-call. Additionally, OpenAI returns \`stop\` even when tool calls are present — you must check for the existence of \`tool\_calls\` in the message before trusting \`stop\` as a completion signal. Gemini adds a third semantic layer with \`FINISH\_REASON\_SAFETY\` which has no direct equivalent in the other two providers' stop reason enums. The only safe approach is a normalization layer.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-20T10:35:00.076079+00:00— report_created — created