From ff19444a6404b30911daecd1b787fac30c25d1d1 Mon Sep 17 00:00:00 2001 From: Arthur Conmy Date: Wed, 24 Dec 2025 23:33:03 +0000 Subject: [PATCH] Handle tool_calls stop reason from OpenAI/OpenRouter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenAI and OpenRouter return "tool_calls" as finish_reason when the model decides to use tools. The field_validator only handled "tool_use" (Anthropic format), causing a ValidationError: ``` ValidationError: 1 validation error for LLMResponse stop_reason Value error, Invalid stop reason: tool_calls [type=value_error, input_value='tool_calls', input_type=str] ``` This is particularly bad because InferenceAPI is commonly used with asyncio.gather() for batch processing. An unhandled exception from one request kills the entire batch, potentially losing hours of work and all in-flight requests. Fix: Map "tool_calls" to StopReason.TOOL_USE in the field_validator. Context: Occurred during single-turn puzzle generation via OpenRouter with gpt-oss-120b, grok-4.1-fast, claude-haiku-4.5. No tools were passed in the request - the model unexpectedly returned tool_calls anyway. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- safetytooling/data_models/inference.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/safetytooling/data_models/inference.py b/safetytooling/data_models/inference.py index 0954c33..4c38aff 100644 --- a/safetytooling/data_models/inference.py +++ b/safetytooling/data_models/inference.py @@ -98,8 +98,8 @@ def parse_stop_reason(cls, v: str): return StopReason.PROMPT_BLOCKED elif v in ["api_error", "error"]: return StopReason.API_ERROR - elif v in ["tool_use"]: - return StopReason.TOOL_USE + elif v in ["tool_use", "tool_calls"]: + return StopReason.TOOL_USE # tool_calls is OpenAI/OpenRouter format elif v in ["recitation"]: return GeminiStopReason.RECITATION elif v in ["safety"]: