Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions src-tauri/src/proxy/providers/streaming_codex_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -442,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();
Comment on lines +447 to 449

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve streaming for tool calls without ids

When an OpenAI-compatible provider omits tool_call.id but sends a valid function.name (which this converter still supports via the call_{index} fallback in finalize_tools), this new condition prevents the tool from being added during streaming. Arguments are buffered silently and the item is only added/done at finalization, so Responses clients that consume response.function_call_arguments.delta no longer see incremental tool-call output for an otherwise valid tool call. The missing-name guard should not also require a non-empty upstream id, or the no-id fallback should emit pending argument deltas when finalizing.

Useful? React with 👍 / 👎.

} else if state.added {
Expand All @@ -464,9 +466,6 @@ impl ChatToResponsesState {
if state.call_id.is_empty() {
state.call_id = format!("call_{chat_index}");
}
if state.name.is_empty() {
state.name = "unknown_tool".to_string();
}
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(
Expand Down Expand Up @@ -699,6 +698,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)
Expand All @@ -713,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,
Expand Down
28 changes: 23 additions & 5 deletions src-tauri/src/proxy/providers/transform_codex_chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1406,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
Expand Down Expand Up @@ -1448,7 +1458,7 @@ fn chat_legacy_function_call_to_response_item(
function_call: &Value,
reasoning: Option<&str>,
tool_context: &CodexToolContext,
) -> Value {
) -> Option<Value> {
let call_id = function_call
.get("id")
.and_then(|v| v.as_str())
Expand All @@ -1458,18 +1468,26 @@ 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,
name,
&arguments,
reasoning,
tool_context,
)
))
}

pub(crate) fn response_tool_call_item_id_from_chat_name(
Expand Down