antirez / antirez/ds4

Responses: apply_patch (and other custom tools) always emitted as function_call — Codex aborts with 'invoked with incompatible payload'

Đang mở
#745 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
C
Star
22.3k
Fork
2.1k
Merge trung bình
1 ngày 3 giờ
Pull request đã merge (30 ngày)
4

Mô tả

## 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`).

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.