From 95a04c8dff6a30227001b48f8dca6bd2ba4a6f99 Mon Sep 17 00:00:00 2001 From: xlplbo Date: Tue, 8 Sep 2026 09:48:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?test(apicompat):=20additional=5Ftools=20?= =?UTF-8?q?=E8=B5=B0=20chat=20=E6=A1=A5=E7=9A=84=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._responses_bridge_additional_tools_test.go | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 backend/internal/pkg/apicompat/chatcompletions_responses_bridge_additional_tools_test.go diff --git a/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_additional_tools_test.go b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_additional_tools_test.go new file mode 100644 index 0000000000..f918f28df1 --- /dev/null +++ b/backend/internal/pkg/apicompat/chatcompletions_responses_bridge_additional_tools_test.go @@ -0,0 +1,52 @@ +package apicompat + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// #6653:Codex 把运行时工具放在 input[].additional_tools 里,force_chat_completions +// 下必须仍降级成标准 chat 工具随 /v1/chat/completions 发出,而不是因无法转换退回 Responses。 +func TestResponsesToChatCompletionsRequest_LowersAdditionalToolsForChatOnlyUpstream(t *testing.T) { + body := `{ + "model":"gpt-5.6-luna","stream":true,"store":false, + "reasoning":{"effort":"medium","context":"all_turns"}, + "tool_choice":"auto","parallel_tool_calls":false, + "include":["reasoning.encrypted_content"], + "input":[ + {"type":"additional_tools","role":"developer","tools":[ + {"type":"custom","name":"exec","description":"run a shell command","format":{"type":"grammar","syntax":"lark","definition":"start: /.*/"}}, + {"type":"function","name":"wait","strict":false,"parameters":{"type":"object"}}, + {"type":"namespace","name":"collaboration","tools":[{"type":"function","name":"spawn_agent","parameters":{"type":"object"}}]} + ]}, + {"type":"message","role":"user","content":[{"type":"input_text","text":"reply ok"}]} + ] + }` + var req ResponsesRequest + require.NoError(t, json.Unmarshal([]byte(body), &req)) + + tools, err := EffectiveResponsesTools(&req) + require.NoError(t, err) + require.Len(t, tools, 3, "additional_tools 里的 custom / function / namespace 都要被提升") + require.True(t, CustomToolNames(tools)["exec"]) + require.Equal(t, "collaboration", NamespaceToolNames(tools)["collaboration__spawn_agent"].Namespace) + + chat, err := ResponsesToChatCompletionsRequest(&req) + require.NoError(t, err) + require.Len(t, chat.Messages, 1, "additional_tools 不能变成 chat 消息") + require.Equal(t, "user", chat.Messages[0].Role) + + names := make([]string, 0, len(chat.Tools)) + for _, tool := range chat.Tools { + require.Equal(t, "function", tool.Type, "chat 上游只认 function 工具") + require.NotNil(t, tool.Function) + names = append(names, tool.Function.Name) + } + require.Equal(t, []string{"exec", "wait", "collaboration__spawn_agent"}, names) + require.JSONEq(t, customToolInputSchema, string(chat.Tools[0].Function.Parameters), "custom 工具降级为单一 input 字符串参数") + require.NotNil(t, chat.ParallelToolCalls) + require.False(t, *chat.ParallelToolCalls) + require.Equal(t, "medium", chat.ReasoningEffort) +} From df8eceba0c150436835f1b02b74b397df81205ac Mon Sep 17 00:00:00 2001 From: xlplbo Date: Thu, 10 Sep 2026 20:25:01 +0800 Subject: [PATCH 2/2] =?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) +}