Responses: apply_patch (and other custom tools) always emitted as function_call — Codex aborts with 'invoked with incompatible payload'
- Vorherrschende Sprache
- C
- Sterne
- 22.3k
- Forks
- 2.1k
- Ø Merge
- 1 T. 3 Std.
- Gemergte PRs (30 T.)
- 4
Beschreibung
## Summary
`/v1/responses` always serializes tool calls as `{"type":"function_call", ...}` because `responses_tool_items_build()` hard-codes `is_custom = false`. Codex registers `apply_patch` as a freeform `{"type":"custom"}` tool, so the Codex router rejects the returned `function_call` before any file write: `Fatal error: tool apply_patch invoked with incompatible payload`. The agent then reports "patch aborted" and retries in a loop; the file is never touched.
## Environment
- ds4-server from antirez/ds4, DeepSeek-V4-Flash-0731, Metal (Mac Studio M3 Ultra 512G)
- Client: Codex CLI 0.147.0 → `/v1/responses`, model catalog entry with `"apply_patch_tool_type": "freeform"`
## Reproduction / observed
Codex advertises the freeform tool as:
```json
{"type":"custom","name":"apply_patch","format":{"type":"grammar","syntax":"lark"}}
```
ds4 returns the model's call as:
```json
{"type":"function_call","name":"apply_patch","arguments":"{\"patch\":\"*** Begin Patch ...\"}"}
```
Codex router log:
```
Fatal error: tool apply_patch invoked with incompatible payload
```
followed by `Function call output is missing for call id: ...` on the retry.
## Expected
For a `{"type":"custom"}` tool, ds4 should emit a `custom_tool_call` item with `input` containing the raw patch text, and accept the matching `custom_tool_call_output` on the next request.
## Root cause
In `ds4_server.c`, `responses_tool_items_build()` hard-codes `is_custom = false` for every call; the comment even assumes "Codex CLI registers all its tools as function tools", which is false for freeform `apply_patch`:
```c
/* The internal tool_call doesn't track whether it came from a function_call or
* a custom_tool_call (or what tool kind is registered). For round-trip
* correctness with the rare custom_tool_call clients, we preserve any provided
* call_id verbatim and pre-assign a stable fc_id; the discriminator currently
* defaults to function_call because Codex CLI registers all its tools as
* function tools. */
static void responses_tool_items_build(responses_tool_item **out,
const tool_calls *calls,
int starting_output_index) {
...
items[i].is_custom = false;
items[i].output_index = starting_output_index + i;
...
}
```
`responses_append_function_call_item()` then picks the item type and body field from that flag, so every tool is emitted as `function_call` with `arguments`:
```c
const char *item_type = item->is_custom ? "custom_tool_call" : "function_call";
const char *body_field = item->is_custom ? "input" : "arguments";
buf_printf(b,
"{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"%s\",\"name\":",
item->fc_id, item_type, item_status);
...
buf_puts(b, ",\"call_id\":");
json_escape(b, item->call_id);
buf_printf(b, ",\"%s\":", body_field);
```
The input path already accepts both `custom_tool_call` / `custom_tool_call_output`, so only the output serializer lacks the discriminator:
```c
} else if (!strcmp(t, "function_call") || !strcmp(t, "custom_tool_call")) {
tool_call tc = {0};
tc.id = xstrdup(call_id ? call_id : item_id ? item_id : "");
/* function_call uses `arguments` (JSON string); custom_tool_call uses
* `input` (free text). Treat both as the same on-wire argument blob — ... */
const char *args_src = arguments ? arguments :
input_str ? input_str : "{}";
tc.arguments = xstrdup(args_src);
```
```c
} else if (!strcmp(t, "function_call_output") || !strcmp(t, "custom_tool_call_output")) {
chat_msg msg = {0};
msg.role = xstrdup("tool");
msg.content = output ? output : xstrdup("");
output = NULL;
if (call_id || item_id) {
chat_msg_add_tool_call_id(&msg, call_id ? call_id : item_id);
}
chat_msgs_push(msgs, msg);
```
## Impact
- Codex agents on ds4 cannot create/edit files with the built-in `apply_patch`; every write attempt fails before touching disk and the agent loops.
- Any other Responses client registering custom/freeform tools is affected the same way.
## Suggested fix
For Codex/other Responses client custom tools:
- emit `custom_tool_call` with `input` = raw patch text, preserving `call_id`;
- unwrap single-key JSON object arguments such as `{"patch": "..."}` back into raw freeform input (or render custom tools in the DSML prompt so the model emits free text);
- add a regression test for one `apply_patch` round trip (call + matching `custom_tool_call_output`).
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.