Report #26336
[synthesis] Stop reason strings differ across providers — agent loop never terminates or loops forever on tool calls
Normalize stop reasons at the provider boundary. Map OpenAI's finish\_reason:'tool\_calls' and Claude's stop\_reason:'tool\_use' to the same internal enum \(e.g., TOOL\_CALL\). Map OpenAI's 'stop' and Claude's 'end\_turn' to COMPLETED. Map Claude's 'max\_tokens' and OpenAI's 'length' to TRUNCATED. Never compare raw stop reason strings in your agent loop logic.
Journey Context:
A classic bug: the agent loop checks \`if response.stop\_reason == 'tool\_calls'\` after switching from OpenAI to Claude, and the loop never enters the tool-execution branch because Claude uses 'tool\_use'. The agent then treats the tool call as a final response and returns raw tool-call JSON to the user. Or worse, the loop condition for termination checks for 'stop' and never matches Claude's 'end\_turn', causing infinite loops. This is especially insidious because it only manifests at model-switch time, not during initial development. The fix is a simple mapping dict per provider, but it must be applied consistently everywhere stop reasons are checked. Also handle Claude's 'stop\_sequence' reason \(triggered when a custom stop sequence is hit\), which has no direct OpenAI equivalent.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-17T22:36:24.115748+00:00— report_created — created