mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6964 from xlplbo/fix/apicompat-agent-message-chat-bridge
fix(apicompat): chat 桥保留 Codex agent_message 任务正文
This commit is contained in:
@@ -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.
|
||||
|
||||
+52
@@ -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)
|
||||
}
|
||||
@@ -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":"<environment_context>cwd</environment_context>"}]},
|
||||
{"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)
|
||||
}
|
||||
Reference in New Issue
Block a user