From c2337d685751dfa366c9a2cd62520ec830e018d9 Mon Sep 17 00:00:00 2001 From: Cao Xin Date: Thu, 4 Jun 2026 08:33:53 +0800 Subject: [PATCH] fix(codex): preserve custom tool metadata in chat routing (#3644) * fix(codex): preserve custom tool metadata in chat routing * fix(codex): reduce custom tool metadata description overhead * fix(codex): stabilize custom tool metadata descriptions * refactor(codex): remove unreachable custom tool description fallback --- .../proxy/providers/transform_codex_chat.rs | 53 +++++++++++++++++-- 1 file changed, 48 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 8720753c6..906b49eb9 100644 --- a/src-tauri/src/proxy/providers/transform_codex_chat.rs +++ b/src-tauri/src/proxy/providers/transform_codex_chat.rs @@ -40,6 +40,8 @@ const EXTRA_CHAT_PASSTHROUGH_FIELDS: &[&str] = &[ const TOOL_SEARCH_PROXY_NAME: &str = "tool_search"; const CUSTOM_TOOL_INPUT_FIELD: &str = "input"; const CHAT_TOOL_NAME_MAX_LEN: usize = 64; +const CUSTOM_TOOL_INPUT_DESCRIPTION: &str = "Raw string input for the original custom tool. Preserve formatting exactly and follow the original tool definition embedded in the description."; +const CUSTOM_TOOL_PRESERVED_METADATA_HEADING: &str = "Original tool definition:"; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CodexToolKind { @@ -132,10 +134,7 @@ impl CodexToolContext { let Some(name) = responses_tool_name(tool) else { return; }; - let description = tool - .get("description") - .cloned() - .unwrap_or_else(|| json!("Custom Codex tool.")); + let description = json!(responses_custom_tool_description(tool)); let chat_tool = json!({ "type": "function", "function": { @@ -146,7 +145,7 @@ impl CodexToolContext { "properties": { CUSTOM_TOOL_INPUT_FIELD: { "type": "string", - "description": "Input to pass to the custom Codex tool." + "description": CUSTOM_TOOL_INPUT_DESCRIPTION } }, "required": [CUSTOM_TOOL_INPUT_FIELD] @@ -1016,6 +1015,22 @@ fn responses_tool_name(tool: &Value) -> Option { .map(ToString::to_string) } +fn responses_custom_tool_description(tool: &Value) -> String { + let mut description = String::new(); + description.push_str(CUSTOM_TOOL_PRESERVED_METADATA_HEADING); + description.push_str("\n```json\n"); + description.push_str(&serialize_tool_definition_for_description(tool)); + description.push_str("\n```"); + description +} + +fn serialize_tool_definition_for_description(tool: &Value) -> String { + // Keep the embedded definition compact to reduce tool-description token + // overhead for chat-only upstreams, while remaining stable across map + // storage order. + canonical_json_string(tool) +} + fn responses_function_tool_to_chat_tool(tool: &Value, chat_name: &str) -> Option { if tool.get("type").and_then(|v| v.as_str()) != Some("function") { return None; @@ -1857,6 +1872,34 @@ mod tests { ); } + #[test] + fn responses_request_to_chat_preserves_custom_tool_metadata_in_description() { + let input = json!({ + "model": "gpt-5.4", + "tools": [{ + "type": "custom", + "name": "apply_patch", + "description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.", + "format": { + "type": "grammar", + "syntax": "lark", + "definition": "start: begin_patch hunk+ end_patch" + } + }] + }); + + let result = responses_to_chat_completions(input).unwrap(); + let description = result["tools"][0]["function"]["description"] + .as_str() + .unwrap(); + + assert!(description.starts_with("Original tool definition:")); + assert!(!description.contains("Original Codex tool definition")); + assert!(description.contains("\"type\":\"custom\"")); + assert!(description.contains("\"format\":")); + assert!(description.contains("\"syntax\":\"lark\"")); + } + #[test] fn responses_request_to_chat_uses_provider_reasoning_effort_for_deepseek_model() { let input = json!({