From 3ae19f55c006ae3c26bb881ac6f1ff883e46be5b Mon Sep 17 00:00:00 2001 From: hueifeng Date: Sat, 13 Jun 2026 21:16:14 +0800 Subject: [PATCH 1/4] Chat API: skip tool calls with missing function names Some providers send empty or absent function names in streaming tool call deltas. Previously these produced invalid output items. - Don't overwrite accumulated state.name with empty deltas - Skip tool calls that never received a valid name (instead of falling back to 'unknown_tool') - Apply the same defensive guard in finalize_tools and the non-streaming path --- .../proxy/providers/streaming_codex_chat.rs | 26 ++++++++++++++++--- .../proxy/providers/transform_codex_chat.rs | 8 ++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/proxy/providers/streaming_codex_chat.rs b/src-tauri/src/proxy/providers/streaming_codex_chat.rs index 7039c1b27a..3c58ca4fe8 100644 --- a/src-tauri/src/proxy/providers/streaming_codex_chat.rs +++ b/src-tauri/src/proxy/providers/streaming_codex_chat.rs @@ -429,8 +429,10 @@ impl ChatToResponsesState { if let Some(id) = id_delta { state.call_id = id; } - if let Some(name) = name_delta { - state.name = name; + if let Some(ref name) = name_delta { + if !name.is_empty() { + state.name.clone_from(name); + } } if !args_delta.is_empty() { state.arguments.push_str(&args_delta); @@ -465,7 +467,10 @@ impl ChatToResponsesState { state.call_id = format!("call_{chat_index}"); } if state.name.is_empty() { - state.name = "unknown_tool".to_string(); + // Model never provided a valid name across all deltas — skip this tool call + log::warn!("[Codex] Skipping tool call with empty name (no delta provided a name)"); + state.done = true; + return events; } state.output_index = Some(assigned); let is_custom_tool = self.tool_context.is_custom_tool_chat_name(&state.name); @@ -699,6 +704,21 @@ impl ChatToResponsesState { continue; } + // Skip tool calls with missing names (defensive: some models generate + // tool call deltas without providing a valid function name) + let has_bad_name = self + .tools + .get(&key) + .map(|state| state.name.is_empty()) + .unwrap_or(true); + if has_bad_name { + if let Some(state) = self.tools.get_mut(&key) { + state.done = true; + } + log::warn!("[Codex] Skipping streaming tool call with missing name"); + continue; + } + if self .tools .get(&key) diff --git a/src-tauri/src/proxy/providers/transform_codex_chat.rs b/src-tauri/src/proxy/providers/transform_codex_chat.rs index d43809f0fa..5d49d479a6 100644 --- a/src-tauri/src/proxy/providers/transform_codex_chat.rs +++ b/src-tauri/src/proxy/providers/transform_codex_chat.rs @@ -1398,6 +1398,14 @@ fn chat_tool_calls_to_response_output_items( if let Some(tool_calls) = message.get("tool_calls").and_then(|v| v.as_array()) { for (index, tool_call) in tool_calls.iter().enumerate() { + // Skip tool calls with missing function names (defensive: some models + // may generate tool calls without providing a valid name) + let function = tool_call.get("function").unwrap_or(&Value::Null); + let name = function.get("name").and_then(|v| v.as_str()).unwrap_or(""); + if name.is_empty() { + log::warn!("[Codex] Skipping tool call with missing name"); + continue; + } output.push(chat_tool_call_to_response_item( tool_call, index, From 00ad284083489b5ebc4cb695af91e71288cab5f9 Mon Sep 17 00:00:00 2001 From: hueifeng Date: Sun, 14 Jun 2026 17:41:19 +0800 Subject: [PATCH 2/4] Address review: defer empty-name skip to finalization Require both call_id and name before triggering should_add, instead of skipping eagerly when name is absent in the first delta. This handles providers that send id before name, as suggested in the Codex review. --- src-tauri/src/proxy/providers/streaming_codex_chat.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src-tauri/src/proxy/providers/streaming_codex_chat.rs b/src-tauri/src/proxy/providers/streaming_codex_chat.rs index 3c58ca4fe8..ea2330c7eb 100644 --- a/src-tauri/src/proxy/providers/streaming_codex_chat.rs +++ b/src-tauri/src/proxy/providers/streaming_codex_chat.rs @@ -444,7 +444,7 @@ impl ChatToResponsesState { } } - if !state.added && (!state.call_id.is_empty() || !state.name.is_empty()) { + if !state.added && !state.call_id.is_empty() && !state.name.is_empty() { should_add = true; pending_arguments = state.arguments.clone(); } else if state.added { @@ -466,12 +466,6 @@ impl ChatToResponsesState { if state.call_id.is_empty() { state.call_id = format!("call_{chat_index}"); } - if state.name.is_empty() { - // Model never provided a valid name across all deltas — skip this tool call - log::warn!("[Codex] Skipping tool call with empty name (no delta provided a name)"); - state.done = true; - return events; - } state.output_index = Some(assigned); let is_custom_tool = self.tool_context.is_custom_tool_chat_name(&state.name); state.item_id = response_tool_call_item_id_from_chat_name( From 98d6b19663cc35540a908c6eba41954f5b93f692 Mon Sep 17 00:00:00 2001 From: hueifeng Date: Fri, 19 Jun 2026 23:12:16 +0800 Subject: [PATCH 3/4] Guard legacy function_call against empty name Return Option from chat_legacy_function_call_to_response_item, returning None when function_call.name is missing or empty. This covers the legacy message.function_call path that the original guard missed. --- .../proxy/providers/transform_codex_chat.rs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/proxy/providers/transform_codex_chat.rs b/src-tauri/src/proxy/providers/transform_codex_chat.rs index 5d49d479a6..1edc23d701 100644 --- a/src-tauri/src/proxy/providers/transform_codex_chat.rs +++ b/src-tauri/src/proxy/providers/transform_codex_chat.rs @@ -1414,11 +1414,13 @@ fn chat_tool_calls_to_response_output_items( )); } } else if let Some(function_call) = message.get("function_call") { - output.push(chat_legacy_function_call_to_response_item( + if let Some(item) = chat_legacy_function_call_to_response_item( function_call, reasoning, tool_context, - )); + ) { + output.push(item); + } } output @@ -1456,7 +1458,7 @@ fn chat_legacy_function_call_to_response_item( function_call: &Value, reasoning: Option<&str>, tool_context: &CodexToolContext, -) -> Value { +) -> Option { let call_id = function_call .get("id") .and_then(|v| v.as_str()) @@ -1466,10 +1468,18 @@ fn chat_legacy_function_call_to_response_item( .get("name") .and_then(|v| v.as_str()) .unwrap_or(""); + + // Skip legacy function calls with missing names (defensive: some models + // may generate function_call without providing a valid name) + if name.is_empty() { + log::warn!("[Codex] Skipping legacy function_call with missing name"); + return None; + } + let arguments = canonicalize_tool_arguments(function_call.get("arguments")); let item_id = response_tool_call_item_id_from_chat_name(call_id, name, tool_context); - response_tool_call_item_from_chat_name( + Some(response_tool_call_item_from_chat_name( &item_id, "completed", call_id, @@ -1477,7 +1487,7 @@ fn chat_legacy_function_call_to_response_item( &arguments, reasoning, tool_context, - ) + )) } pub(crate) fn response_tool_call_item_id_from_chat_name( From a3526aae85e451995c37edc60d19aebfc682d5c2 Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 19 Jun 2026 23:36:57 +0800 Subject: [PATCH 4/4] Remove unreachable unknown_tool fallback --- src-tauri/src/proxy/providers/streaming_codex_chat.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src-tauri/src/proxy/providers/streaming_codex_chat.rs b/src-tauri/src/proxy/providers/streaming_codex_chat.rs index ea2330c7eb..a5326ab137 100644 --- a/src-tauri/src/proxy/providers/streaming_codex_chat.rs +++ b/src-tauri/src/proxy/providers/streaming_codex_chat.rs @@ -727,9 +727,6 @@ impl ChatToResponsesState { if state.call_id.is_empty() { state.call_id = format!("call_{key}"); } - if state.name.is_empty() { - state.name = "unknown_tool".to_string(); - } state.output_index = Some(assigned); state.item_id = response_tool_call_item_id_from_chat_name( &state.call_id,