From df8eceba0c150436835f1b02b74b397df81205ac Mon Sep 17 00:00:00 2001 From: xlplbo Date: Thu, 10 Sep 2026 20:25:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(apicompat):=20chat=20=E6=A1=A5=E4=BF=9D?= =?UTF-8?q?=E7=95=99=20Codex=20agent=5Fmessage=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=AD=A3=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatcompletions_responses_bridge.go | 41 +++++++++++++ ...ons_responses_bridge_agent_message_test.go | 58 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 backend/internal/pkg/apicompat/chatcompletions_responses_bridge_agent_message_test.go diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go index e9f7ff0354..cacd76018f 100644 --- a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge.go @@ -523,6 +523,21 @@ func buildChatMessagesFromItems(messages []ChatMessage, rawItems []json.RawMessa }) pendingReasoning = "" continue + case "agent_message": + // Codex multi_agent_v2 用 agent_message 在父线程与子智能体之间传递任务和回复: + // input_text 是信封(消息类型、任务名、发送者),正文放在 encrypted_content 片段里 + // (自定义 provider 下为明文)。chat 上游没有对应条目,按原顺序拼成一条 user 消息, + // 否则子智能体收不到任务却仍返回 200。 + text := agentMessageText(item["content"]) + if text == "" { + pendingReasoning = "" + continue + } + content, _ := json.Marshal(text) + messages = append(messages, ChatMessage{Role: "user", Content: content}) + pendingReasoning = "" + lastTurnReasoning = "" + continue case "input_text", "text": content, _ := json.Marshal(rawString(item["text"])) messages = append(messages, ChatMessage{Role: "user", Content: content}) @@ -582,6 +597,32 @@ func buildChatMessagesFromItems(messages []ChatMessage, rawItems []json.RawMessa return messages, mediaByCallID, nil } +// agentMessageText 按原顺序拼接 agent_message 里 input_text 与 encrypted_content 片段的文本。 +func agentMessageText(raw json.RawMessage) string { + raw = bytesTrimSpace(raw) + if len(raw) == 0 || string(raw) == "null" { + return "" + } + var text string + if err := json.Unmarshal(raw, &text); err == nil { + return text + } + var parts []map[string]json.RawMessage + if err := json.Unmarshal(raw, &parts); err != nil { + return "" + } + var b strings.Builder + for _, part := range parts { + switch rawString(part["type"]) { + case "input_text", "text": + _, _ = b.WriteString(rawString(part["text"])) + case "encrypted_content": + _, _ = b.WriteString(rawString(part["encrypted_content"])) + } + } + return b.String() +} + // extractToolOutputMedia rewrites only recognized image nodes. Media-free // outputs return rewritten=false so the caller can preserve their original // bytes and prompt-cache prefix. diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_agent_message_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_agent_message_test.go new file mode 100644 index 0000000000..d51cd26a27 --- /dev/null +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_agent_message_test.go @@ -0,0 +1,58 @@ +package apicompat + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// #6887:Codex multi_agent_v2 用 agent_message 条目在父线程与子智能体之间传递任务和回复, +// 任务正文放在 encrypted_content 片段里(自定义 provider 下为明文)。chat 上游没有对应条目, +// 桥必须把它按原顺序拼成一条 user 消息,否则子智能体收不到任务。 +func TestResponsesToChatCompletionsRequest_AgentMessageBecomesUserMessage(t *testing.T) { + req := &ResponsesRequest{ + Model: "glm-5.3", + Input: json.RawMessage(`[ + {"type":"message","role":"user","content":[{"type":"input_text","text":"cwd"}]}, + {"type":"agent_message","id":"amsg_1","author":"/root","recipient":"/root/alpha_task","content":[ + {"type":"input_text","text":"Message Type: NEW_TASK\nTask name: /root/alpha_task\nSender: /root\nPayload:\n"}, + {"type":"encrypted_content","encrypted_content":"Reply with the single word: ALPHA"} + ]}, + {"type":"reasoning","summary":[{"type":"summary_text","text":"just answer"}]}, + {"type":"message","role":"assistant","content":[{"type":"output_text","text":"ALPHA"}]}, + {"type":"agent_message","id":"amsg_2","author":"/root/alpha_task","recipient":"/root","content":[ + {"type":"input_text","text":"Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/alpha_task\nPayload:\nALPHA"} + ]} + ]`), + } + + out, err := ResponsesToChatCompletionsRequest(req) + require.NoError(t, err) + require.Len(t, out.Messages, 4) + + require.Equal(t, "user", out.Messages[0].Role) + require.Equal(t, "user", out.Messages[1].Role, "父线程发来的任务要成为 user 消息") + require.JSONEq(t, `"Message Type: NEW_TASK\nTask name: /root/alpha_task\nSender: /root\nPayload:\nReply with the single word: ALPHA"`, string(out.Messages[1].Content), "信封与 encrypted_content 里的正文要按原顺序拼接") + require.Equal(t, "assistant", out.Messages[2].Role) + require.JSONEq(t, `"ALPHA"`, string(out.Messages[2].Content)) + require.Equal(t, "user", out.Messages[3].Role, "子智能体回给父线程的消息同样是 user 消息") + require.JSONEq(t, `"Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/alpha_task\nPayload:\nALPHA"`, string(out.Messages[3].Content)) +} + +func TestResponsesToChatCompletionsRequest_AgentMessageWithoutTextIsSkipped(t *testing.T) { + req := &ResponsesRequest{ + Model: "glm-5.3", + Input: json.RawMessage(`[ + {"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}, + {"type":"agent_message","author":"/root","recipient":"/root/a","content":[]}, + {"type":"agent_message","author":"/root","recipient":"/root/a"}, + {"type":"agent_message","author":"/root","recipient":"/root/a","content":[{"type":"input_image","image_url":"data:image/png;base64,AA=="}]} + ]`), + } + + out, err := ResponsesToChatCompletionsRequest(req) + require.NoError(t, err) + require.Len(t, out.Messages, 1, "没有文本正文的 agent_message 不产生消息") + require.Equal(t, "user", out.Messages[0].Role) +}