Support model-specific function description prefixes (#51930)

## What changed

Add `functions_namespace_functions_description_prefixes` to model tool messages, keyed by unqualified tool name. Prepend this guidance to descriptions in the `functions` namespace across direct declarations, Code Mode documentation, and tool-search results, including custom tools.

Trim prefixes, ignore empty values and unavailable functions, and enforce a combined 256-byte UTF-8 limit including separators for reachable tools. Keep overrides local to each step, preserve search ranking text, and retain runtime caches for unaffected tools.

## Testing

Extend coverage for direct and indirect descriptions, clearing prefixes when switching models, namespace scoping for deferred dynamic tools, and tool-search cache reuse and reset.

GitOrigin-RevId: f434e37cec010764fe3e09fb12c339e933b3c21c
This commit is contained in:
tongzhou wang
2026-10-08 03:20:54 +00:00
committed by copyberry
parent 82e70121f8
commit e974aad3b1
15 changed files with 527 additions and 134 deletions
@@ -0,0 +1,83 @@
//! Prepares function descriptions before direct, Code Mode, or search rendering.
//! Only the `functions` namespace is affected; runtime-owned specs stay unchanged.
use crate::ResponsesApiNamespaceTool;
use crate::ToolName;
use crate::ToolSpec;
use std::collections::BTreeMap;
/// Trimmed, size-bounded description prefixes for the available `functions.*` tools.
#[derive(Default)]
pub struct FunctionsNamespaceFunctionPrefixes(BTreeMap<String, String>);
impl FunctionsNamespaceFunctionPrefixes {
pub fn new(
prefixes: Option<&BTreeMap<String, String>>,
available_tools: impl IntoIterator<Item = ToolName>,
) -> Result<Self, &'static str> {
let Some(prefixes) = prefixes.filter(|prefixes| !prefixes.is_empty()) else {
return Ok(Self::default());
};
let mut remaining = 256usize;
let mut normalized = BTreeMap::new();
for tool in available_tools {
if !tool.is_default_namespace() || normalized.contains_key(&tool.name) {
continue;
}
let Some(prefix) = prefixes.get(&tool.name) else {
continue;
};
let prefix = prefix.trim();
if prefix.is_empty() {
continue;
}
remaining = remaining
.checked_sub(prefix.len())
.and_then(|remaining| remaining.checked_sub(2))
.ok_or(
"Available functions' description prefixes exceed the 256-byte combined limit",
)?;
normalized.insert(tool.name, prefix.to_owned());
}
Ok(Self(normalized))
}
/// Returns an owned replacement only for a configured function. Unaffected tools
/// keep their original specs and derived caches, including large MCP schemas.
pub fn prepare(&self, name: &ToolName, spec: impl FnOnce() -> ToolSpec) -> Option<ToolSpec> {
if !name.is_default_namespace() || !self.0.contains_key(&name.name) {
return None;
}
let mut spec = spec();
match &mut spec {
ToolSpec::Function(tool) => self.prepend(&tool.name, &mut tool.description),
ToolSpec::Freeform(tool) => self.prepend(&tool.name, &mut tool.description),
ToolSpec::Namespace(namespace) if namespace.name == "functions" => {
for tool in &mut namespace.tools {
match tool {
ResponsesApiNamespaceTool::Function(tool) => {
self.prepend(&tool.name, &mut tool.description);
}
ResponsesApiNamespaceTool::Custom(tool) => {
self.prepend(&tool.name, &mut tool.description);
}
}
}
}
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
return None;
}
}
Some(spec)
}
fn prepend(&self, name: &str, description: &mut String) {
if let Some(prefix) = self.0.get(name) {
*description = if description.is_empty() {
prefix.clone()
} else {
format!("{prefix}\n\n{description}")
};
}
}
}
+2
View File
@@ -4,6 +4,7 @@
mod code_mode;
mod dynamic_tool;
mod function_call_error;
mod functions_namespace_function_prefixes;
mod image_detail;
mod indirect_namespace_prefixes;
mod json_schema;
@@ -30,6 +31,7 @@ pub use code_mode::tool_spec_to_code_mode_tool_definition;
pub use codex_protocol::ToolName;
pub use dynamic_tool::parse_dynamic_tool;
pub use function_call_error::FunctionCallError;
pub use functions_namespace_function_prefixes::FunctionsNamespaceFunctionPrefixes;
pub use image_detail::can_request_original_image_detail;
pub use image_detail::normalize_output_image_detail;
pub use image_detail::sanitize_original_image_detail;