From 8041b5beb3afa64df6f0273697c5aede5936da4a Mon Sep 17 00:00:00 2001 From: centdix <40307056+centdix@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:06:33 +0100 Subject: [PATCH] fix(aiagent): force structured tool usage for claude (#7052) --- .../src/ai/providers/openai.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/backend/windmill-worker/src/ai/providers/openai.rs b/backend/windmill-worker/src/ai/providers/openai.rs index 4fc4065766..d764cc11ad 100644 --- a/backend/windmill-worker/src/ai/providers/openai.rs +++ b/backend/windmill-worker/src/ai/providers/openai.rs @@ -8,6 +8,7 @@ use crate::ai::{ query_builder::{BuildRequestArgs, ParsedResponse, QueryBuilder, StreamEventProcessor}, sse::{OpenAISSEParser, SSEParser}, types::*, + utils::is_claude_model, }; // OpenAI-specific types @@ -79,6 +80,14 @@ pub struct OpenAIImageOutput { pub result: Option, // Base64 encoded image, None if not completed } +#[derive(Serialize, Debug)] +#[serde(rename_all = "lowercase")] +pub enum ToolChoice { + #[allow(dead_code)] + Auto, + Required, +} + #[derive(Serialize)] pub struct OpenAIRequest<'a> { pub model: &'a str, @@ -91,6 +100,8 @@ pub struct OpenAIRequest<'a> { pub max_completion_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] pub response_format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_choice: Option, pub stream: bool, } @@ -192,6 +203,14 @@ impl OpenAIQueryBuilder { None }; + let is_claude_model = is_claude_model(&args.model); + // Force usage of structured output tool for Claude models when structured output provided + let tool_choice = if is_claude_model && response_format.is_some() { + Some(ToolChoice::Required) + } else { + None + }; + let request = OpenAIRequest { model: args.model, messages: &prepared_messages, @@ -199,6 +218,7 @@ impl OpenAIQueryBuilder { temperature: args.temperature, max_completion_tokens: args.max_tokens, response_format, + tool_choice, stream, };