From a6863c64409944542d64646ba8279f10e15c867c Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 12 Oct 2026 11:19:56 +0200 Subject: [PATCH] fix: keep a static empty override from reading caller arguments or the saved effort --- backend/windmill-worker/src/ai_executor.rs | 15 ++++++++++++--- .../flows/conversations/agentChatInputs.test.ts | 5 +++++ .../flows/conversations/agentChatInputs.ts | 9 +++++---- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/backend/windmill-worker/src/ai_executor.rs b/backend/windmill-worker/src/ai_executor.rs index 2a746ed03d..01ac2becdd 100644 --- a/backend/windmill-worker/src/ai_executor.rs +++ b/backend/windmill-worker/src/ai_executor.rs @@ -281,12 +281,15 @@ fn override_provider_model( InputTransform::Javascript { .. } | InputTransform::Static { .. } ) }) - .map(|_| { - local_args + .map(|t| match t { + // A static null is not evaluated for a tool, which leaves the caller's argument + // in its place. + InputTransform::Static { value } if value.get() == "null" => String::new(), + _ => local_args .get(key) .and_then(|v| serde_json::from_str::(v.get()).ok()) .map(|s| s.trim().to_string()) - .unwrap_or_default() + .unwrap_or_default(), }) }; if let Some(model) = authored("model").filter(|m| !m.is_empty()) { @@ -3015,6 +3018,12 @@ mod tests { let sent = serde_json::from_value(json!({ "model": "other" })).unwrap(); override_provider_model(&mut provider, &sent, &HashMap::new()); assert_eq!(provider.model, "saved-model"); + let null = HashMap::from([( + "model".to_string(), + InputTransform::Static { value: to_raw_value(&serde_json::Value::Null) }, + )]); + override_provider_model(&mut provider, &sent, &null); + assert_eq!(provider.model, "saved-model"); assert_eq!(run(json!({})), saved); assert_eq!(run(json!({ "model": "" })), saved); diff --git a/frontend/src/lib/components/flows/conversations/agentChatInputs.test.ts b/frontend/src/lib/components/flows/conversations/agentChatInputs.test.ts index e60d00d390..e5ea7d6211 100644 --- a/frontend/src/lib/components/flows/conversations/agentChatInputs.test.ts +++ b/frontend/src/lib/components/flows/conversations/agentChatInputs.test.ts @@ -541,6 +541,11 @@ describe('a step linked to a saved agent', () => { saved )! expect(wiredFieldValue(wiring, {}, 'reasoning_effort')).toBe('high') + const none = resolveAgentModelWiring( + [linked({ reasoning_effort: { type: 'static', value: '' } })], + saved + )! + expect(wiredFieldValue(none, {}, 'reasoning_effort')).toBe('') expect( wiredFieldValue(wiring, { model: 'claude-haiku-4-5' }, 'reasoning_effort') ).toBeUndefined() diff --git a/frontend/src/lib/components/flows/conversations/agentChatInputs.ts b/frontend/src/lib/components/flows/conversations/agentChatInputs.ts index e094a534cd..383824165b 100644 --- a/frontend/src/lib/components/flows/conversations/agentChatInputs.ts +++ b/frontend/src/lib/components/flows/conversations/agentChatInputs.ts @@ -126,7 +126,8 @@ function parseLinkedStep( if (!transform) continue if (transform.type === 'static') { const value = typeof transform.value === 'string' ? transform.value.trim() : '' - if (value) fixed[field] = value + // An empty model keeps the saved one; an empty effort is the step's choice of none. + if (value || field === 'reasoning_effort') fixed[field] = value continue } if (transform.type !== 'javascript') return undefined @@ -150,10 +151,10 @@ export function wiredFieldValue( if (wiring.whole) return values[wiring.whole]?.[field] const name = wiring.fields[field] const own = name ? values[name] : wiring.fixed[field] - if (!wiring.saved || (own !== undefined && own !== '')) return own - if (field === 'model') return wiring.saved.model + if (!wiring.saved) return own + if (field === 'model') return own || wiring.saved.model // An effort the step overrides is the run's whatever it holds: empty there is no effort. - if (field === 'reasoning_effort' && !name) { + if (field === 'reasoning_effort' && !name && own === undefined) { return wiredFieldValue(wiring, values, 'model') === wiring.saved.model ? wiring.saved.reasoning_effort : own