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
This commit is contained in:
Cao Xin
2026-06-04 08:33:53 +08:00
committed by GitHub
parent ce538265cb
commit c2337d6857
@@ -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<String> {
.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<Value> {
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!({