mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #4003 from wucm667/fix/codex-message-item-id-prefix
fix(codex): 剥离续链 message item 的非法 item_* id
This commit is contained in:
@@ -114,14 +114,15 @@ func TestFilterCodexInput_OutputTypeKeepsItemID(t *testing.T) {
|
||||
require.Equal(t, "o1", out["id"], "output item id should be preserved")
|
||||
}
|
||||
|
||||
// TestFilterCodexInput_NonToolCallItemKeepsID ensures non-tool-call items
|
||||
// (e.g. message) still keep their id when PreserveReferences is true.
|
||||
// TestFilterCodexInput_NonToolCallItemKeepsID ensures items subject to neither
|
||||
// the fc* (call-input) nor the msg* (message) prefix rule still keep their id
|
||||
// when PreserveReferences is true.
|
||||
// message is covered separately in openai_codex_message_item_id_test.go (#3981).
|
||||
func TestFilterCodexInput_NonToolCallItemKeepsID(t *testing.T) {
|
||||
input := []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": "item_msg_001",
|
||||
"role": "user",
|
||||
"type": "web_search_call",
|
||||
"id": "ws_001",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ func TestFilterCodexInput_NonToolCallItemKeepsID(t *testing.T) {
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 1)
|
||||
msg, ok := filtered[0].(map[string]any)
|
||||
item, ok := filtered[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "item_msg_001", msg["id"], "non-tool-call items keep their id in preserve mode")
|
||||
require.Equal(t, "ws_001", item["id"], "unconstrained items keep their id in preserve mode")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestFilterCodexInput_StripsMessageItemID_WhenPreservingReferences
|
||||
// verifies that message items with a non-msg id (e.g. item_*) have their id
|
||||
// stripped even when PreserveReferences is true. OpenAI upstream requires
|
||||
// message ids to begin with "msg" and rejects item_* with 400:
|
||||
// "Expected an ID that begins with 'msg'." (#3981)
|
||||
func TestFilterCodexInput_StripsMessageItemID_WhenPreservingReferences(t *testing.T) {
|
||||
input := []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": "item_3bc5a3fa8ccde25f1c0000d4",
|
||||
"role": "user",
|
||||
"content": []any{
|
||||
map[string]any{"type": "input_text", "text": "hello"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: true,
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 1)
|
||||
|
||||
msg, ok := filtered[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "message", msg["type"])
|
||||
_, hasID := msg["id"]
|
||||
require.False(t, hasID, "item_* id should be stripped from message")
|
||||
require.Equal(t, "user", msg["role"], "role must be preserved")
|
||||
require.NotNil(t, msg["content"], "content must be preserved")
|
||||
}
|
||||
|
||||
// TestFilterCodexInput_KeepsMsgID_WhenPreservingReferences
|
||||
// verifies that message items with a valid msg* id are kept when
|
||||
// PreserveReferences is true, so context references are not lost.
|
||||
func TestFilterCodexInput_KeepsMsgID_WhenPreservingReferences(t *testing.T) {
|
||||
input := []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": "msg_validID123",
|
||||
"role": "assistant",
|
||||
},
|
||||
}
|
||||
|
||||
filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: true,
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 1)
|
||||
msg, ok := filtered[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "msg_validID123", msg["id"], "valid msg* id must be preserved")
|
||||
}
|
||||
|
||||
// TestFilterCodexInput_StripsMessageIDWhenNotPreservingReferences ensures the
|
||||
// non-continuation path still drops every message id regardless of prefix.
|
||||
func TestFilterCodexInput_StripsMessageIDWhenNotPreservingReferences(t *testing.T) {
|
||||
for _, id := range []string{"item_abc", "msg_validID123"} {
|
||||
input := []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": id,
|
||||
"role": "user",
|
||||
},
|
||||
}
|
||||
|
||||
filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: false,
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 1)
|
||||
msg, ok := filtered[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
_, hasID := msg["id"]
|
||||
require.False(t, hasID, "id %q should be stripped when not preserving references", id)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFilterCodexInput_MessageIDStripDoesNotMutateInput ensures the original
|
||||
// input map is not modified in place when the id is stripped.
|
||||
func TestFilterCodexInput_MessageIDStripDoesNotMutateInput(t *testing.T) {
|
||||
original := map[string]any{
|
||||
"type": "message",
|
||||
"id": "item_abc",
|
||||
"role": "user",
|
||||
}
|
||||
|
||||
filtered := filterCodexInputWithOptions([]any{original}, codexInputFilterOptions{
|
||||
PreserveReferences: true,
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 1)
|
||||
require.Equal(t, "item_abc", original["id"], "original input must not be mutated")
|
||||
}
|
||||
|
||||
// TestFilterCodexInput_MessageStripKeepsFunctionCallBehavior guards against a
|
||||
// regression of #3785: message and function_call id rules are independent.
|
||||
func TestFilterCodexInput_MessageStripKeepsFunctionCallBehavior(t *testing.T) {
|
||||
input := []any{
|
||||
map[string]any{
|
||||
"type": "message",
|
||||
"id": "item_msg_001",
|
||||
"role": "user",
|
||||
},
|
||||
map[string]any{
|
||||
"type": "function_call",
|
||||
"id": "fc_validID123",
|
||||
"call_id": "fc_validID123",
|
||||
"name": "bash",
|
||||
},
|
||||
map[string]any{
|
||||
"type": "function_call",
|
||||
"id": "item_A9v0SNfS3VaLrfX0j3y4xhyK",
|
||||
"call_id": "fc_abc123",
|
||||
"name": "bash",
|
||||
},
|
||||
map[string]any{
|
||||
"type": "function_call_output",
|
||||
"id": "o1",
|
||||
"call_id": "fc_abc123",
|
||||
"output": "done",
|
||||
},
|
||||
}
|
||||
|
||||
filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
|
||||
PreserveReferences: true,
|
||||
})
|
||||
|
||||
require.Len(t, filtered, 4)
|
||||
|
||||
msg, ok := filtered[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
_, hasID := msg["id"]
|
||||
require.False(t, hasID, "message item_* id should be stripped")
|
||||
|
||||
fcValid, ok := filtered[1].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "fc_validID123", fcValid["id"], "valid fc* id must be preserved")
|
||||
|
||||
fcBad, ok := filtered[2].(map[string]any)
|
||||
require.True(t, ok)
|
||||
_, hasID = fcBad["id"]
|
||||
require.False(t, hasID, "function_call item_* id should still be stripped")
|
||||
require.Equal(t, "fc_abc123", fcBad["call_id"], "call_id pairing must survive")
|
||||
|
||||
out, ok := filtered[3].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "o1", out["id"], "output item id should be preserved")
|
||||
require.Equal(t, "fc_abc123", out["call_id"], "call_id pairing must survive")
|
||||
}
|
||||
@@ -1405,6 +1405,15 @@ func filterCodexInputWithOptions(input []any, opts codexInputFilterOptions) []an
|
||||
ensureCopy()
|
||||
delete(newItem, "id")
|
||||
}
|
||||
} else if typ == "message" {
|
||||
// 同理,message 类 item 的 id 必须以 "msg" 开头(上游校验
|
||||
// "Expected an ID that begins with 'msg'")。item_* 形式的 id
|
||||
// 来自客户端回放,需要删除。
|
||||
// 注意:不改写成 msg_*,改写出的 id 未必对应真实的上游对象。
|
||||
if id, ok := m["id"].(string); ok && id != "" && !strings.HasPrefix(id, "msg") {
|
||||
ensureCopy()
|
||||
delete(newItem, "id")
|
||||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, newItem)
|
||||
|
||||
Reference in New Issue
Block a user