language_model: Add OpenAI Responses API custom tool support (#60638)

# Objective

Allow zed's language model stack to express OpenAI Responses API custom
tools (freeform text-input tools with an optional lark/regex grammar),
so downstream consumers can offer tools like a freeform `apply_patch` to
GPT models.
## Solution

- `LanguageModelRequestTool` now carries a Function-vs-Custom input
variant; `LanguageModelCustomToolFormat` models text/grammar formats.
- `LanguageModelToolUse.input` becomes a typed
`LanguageModelToolUseInput::{Json, Text}`. Serialization is tagged so
persisted Text inputs round-trip losslessly; legacy plain JSON values
still deserialize as `Json`.
- `open_ai` gains the custom tool wire types (tool definition,
`custom_tool_call`/`custom_tool_call_output` input items with
string-or-content-part outputs, output item, and
`custom_tool_call_input` delta/done stream events). The Responses event
mapper accumulates raw text deltas into `ToolUse` events, and history
replay derives custom-vs-function tool results from the matching
`ToolUse` by id.
- All non-OpenAI providers and the Chat Completions path error
explicitly when a request contains custom tools — no silent drops or
empty-schema coercions.

Release Notes:

- N/A
This commit is contained in:
Ben Kunkle authored and GitHub committed 2026-07-09 12:05:50 +00:00
1 parent 55b776183d
commit 3e976e89bc
31 files changed
+1652 -501

No files matched your search

+17 -19
View File
@@ -525,18 +525,18 @@ impl CodegenAlternative {
messages.push(user_message);
let tools = vec![
LanguageModelRequestTool {
name: REWRITE_SECTION_TOOL_NAME.to_string(),
description: "Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
input_schema: language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
use_input_streaming: false,
},
LanguageModelRequestTool {
name: FAILURE_MESSAGE_TOOL_NAME.to_string(),
description: "Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
input_schema: language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
use_input_streaming: false,
},
LanguageModelRequestTool::function(
REWRITE_SECTION_TOOL_NAME.to_string(),
"Replaces text in <rewrite_this></rewrite_this> tags with your replacement_text.".to_string(),
language_model::tool_schema::root_schema_for::<RewriteSectionInput>(tool_input_format).to_value(),
false,
),
LanguageModelRequestTool::function(
FAILURE_MESSAGE_TOOL_NAME.to_string(),
"Use this tool to provide a message to the user when you're unable to complete a task.".to_string(),
language_model::tool_schema::root_schema_for::<FailureMessageInput>(tool_input_format).to_value(),
false,
),
];
LanguageModelRequest {
@@ -1179,9 +1179,7 @@ impl CodegenAlternative {
let mut chars_read_by_tool_id = chars_read_by_tool_id.lock();
match tool_use.name.as_ref() {
REWRITE_SECTION_TOOL_NAME => {
let Ok(input) =
serde_json::from_value::<RewriteSectionInput>(tool_use.input)
else {
let Ok(input) = tool_use.input.parse::<RewriteSectionInput>() else {
return None;
};
let chars_read_so_far =
@@ -1198,9 +1196,7 @@ impl CodegenAlternative {
})
}
FAILURE_MESSAGE_TOOL_NAME => {
let Ok(mut input) =
serde_json::from_value::<FailureMessageInput>(tool_use.input)
else {
let Ok(mut input) = tool_use.input.parse::<FailureMessageInput>() else {
return None;
};
Some(ToolUseOutput::Failure(std::mem::take(&mut input.message)))
@@ -2011,7 +2007,9 @@ mod tests {
id: id.into(),
name: REWRITE_SECTION_TOOL_NAME.into(),
raw_input: serde_json::to_string(&input).unwrap(),
input: serde_json::to_value(&input).unwrap(),
input: language_model::LanguageModelToolUseInput::Json(
serde_json::to_value(&input).unwrap(),
),
is_input_complete: is_complete,
thought_signature: None,
})