From 548267a0d273d41f69a982de8bd5c840a4ab0f78 Mon Sep 17 00:00:00 2001 From: Hackall <36754621+hackall360@users.noreply.github.com> Date: Sat, 27 Sep 2025 08:06:45 -0700 Subject: [PATCH] Handle legacy apply_patch arguments from LM Studio --- codex-rs/core/src/chat_completions.rs | 3 +-- codex-rs/core/src/codex.rs | 7 +++++- codex-rs/core/src/openai_tools.rs | 34 +++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 96977150fc8..c611eb20a51 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -690,8 +690,7 @@ mod tests { assert!(!call_id.is_empty(), "call_id should not be empty"); assert!( call_id.starts_with("tool_call_"), - "unexpected fallback call_id prefix: {}", - call_id + "unexpected fallback call_id prefix: {call_id}" ); } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 8b792887c0b..8b7e3cfbca9 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2398,8 +2398,13 @@ async fn handle_function_call( "failed to parse function arguments: {e:?}" )) })?; + let patch = args.into_patch().map_err(|msg| { + FunctionCallError::RespondToModel(format!( + "unsupported apply_patch arguments: {msg}" + )) + })?; let exec_params = ExecParams { - command: vec!["apply_patch".to_string(), args.input.clone()], + command: vec!["apply_patch".to_string(), patch], cwd: turn_context.cwd.clone(), timeout_ms: None, env: HashMap::new(), diff --git a/codex-rs/core/src/openai_tools.rs b/codex-rs/core/src/openai_tools.rs index 30d3bada797..bf32120f225 100644 --- a/codex-rs/core/src/openai_tools.rs +++ b/codex-rs/core/src/openai_tools.rs @@ -277,8 +277,38 @@ fn create_view_image_tool() -> OpenAiTool { } /// TODO(dylan): deprecate once we get rid of json tool #[derive(Serialize, Deserialize)] -pub(crate) struct ApplyPatchToolArgs { - pub(crate) input: String, +#[serde(untagged)] +pub(crate) enum ApplyPatchToolArgs { + /// Preferred shape where the patch contents are carried in the `input` field. + Input { input: String }, + /// Legacy payloads that used a `patch` field. + LegacyPatch { patch: String }, + /// Some older clients pass a shell-style command array. Accept the second + /// element as the patch contents when present. + LegacyCommand { command: Vec }, + /// Bare string arguments emitted by certain OSS backends. + Raw(String), +} + +impl ApplyPatchToolArgs { + pub(crate) fn into_patch(self) -> Result { + match self { + ApplyPatchToolArgs::Input { input } => Ok(input), + ApplyPatchToolArgs::LegacyPatch { patch } => Ok(patch), + ApplyPatchToolArgs::LegacyCommand { command } => { + if command.len() < 2 { + return Err("apply_patch command array missing patch contents".to_string()); + } + if command.first().map(String::as_str) != Some("apply_patch") { + return Err( + "apply_patch command array must start with `apply_patch`".to_string() + ); + } + Ok(command[1].clone()) + } + ApplyPatchToolArgs::Raw(raw) => Ok(raw), + } + } } /// Returns JSON values that are compatible with Function Calling in the