feat(providers/openai_responses_common): prefix user content with sender name
The OpenAI Responses API (used by Codex and the Azure responses endpoint) is a separate wire format from Chat Completions. The SDK type used here, EasyInputMessageParam, does not currently surface a per-message name field, so the same in-content prefix fallback used for Anthropic and Bedrock applies here too. Both branches of the user-message path are covered: - The plain-text branch (OfMessage / EasyInputMessageParam) prefixes msg.Content via messageutil.ApplyUserNamePrefix. - The multipart branch (OfInputMessage / ResponseInputItemMessageParam, used when media is attached) prefixes the text portion before passing it to BuildMultipartContent. The function-call-output branch (msg.ToolCallID set) is intentionally left untouched; tool results are not user utterances and must not gain a sender prefix. Tests cover all three paths (plain-text prefix, multipart prefix, tool result untouched) by JSON-marshalling the resulting input and asserting on the wire bytes. Refs #2702.
This commit is contained in:
parent
5b0634286c
commit
ce0f526bf1
2 changed files with 75 additions and 3 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/openai/openai-go/v3/responses"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/providers/common"
|
||||
"github.com/sipeed/picoclaw/pkg/providers/messageutil"
|
||||
"github.com/sipeed/picoclaw/pkg/providers/protocoltypes"
|
||||
)
|
||||
|
||||
|
|
@ -36,7 +37,10 @@ func TranslateMessages(messages []protocoltypes.Message) (input responses.Respon
|
|||
},
|
||||
})
|
||||
} else if len(msg.Media) > 0 {
|
||||
content := BuildMultipartContent(msg.Content, msg.Media)
|
||||
// Render sender attribution as a `[name] ` prefix on the
|
||||
// text portion of the multipart payload. The persisted
|
||||
// message is not mutated.
|
||||
content := BuildMultipartContent(messageutil.ApplyUserNamePrefix(msg), msg.Media)
|
||||
input = append(input, responses.ResponseInputItemUnionParam{
|
||||
OfInputMessage: &responses.ResponseInputItemMessageParam{
|
||||
Role: "user",
|
||||
|
|
@ -46,8 +50,10 @@ func TranslateMessages(messages []protocoltypes.Message) (input responses.Respon
|
|||
} else {
|
||||
input = append(input, responses.ResponseInputItemUnionParam{
|
||||
OfMessage: &responses.EasyInputMessageParam{
|
||||
Role: responses.EasyInputMessageRoleUser,
|
||||
Content: responses.EasyInputMessageContentUnionParam{OfString: openai.Opt(msg.Content)},
|
||||
Role: responses.EasyInputMessageRoleUser,
|
||||
Content: responses.EasyInputMessageContentUnionParam{
|
||||
OfString: openai.Opt(messageutil.ApplyUserNamePrefix(msg)),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -577,3 +577,69 @@ func TestTranslateTools_SerializesToJSON(t *testing.T) {
|
|||
t.Errorf("JSON should contain web_search, got: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sender attribution (Name field) tests ---
|
||||
|
||||
// TestTranslateMessages_UserNamePrefixed verifies that sender attribution
|
||||
// from Message.Name is rendered as a `[name] ` prefix on plain-text user
|
||||
// messages sent to the OpenAI Responses API. The API supports per-message
|
||||
// name natively, but the SDK type used here (EasyInputMessageParam) does
|
||||
// not surface it, so prefixing is the consistent fallback shared with
|
||||
// Anthropic / Bedrock adapters.
|
||||
func TestTranslateMessages_UserNamePrefixed(t *testing.T) {
|
||||
msgs := []protocoltypes.Message{
|
||||
{Role: "user", Content: "My name is Alice", Name: "U_alice"},
|
||||
}
|
||||
input, _ := TranslateMessages(msgs)
|
||||
if len(input) != 1 || input[0].OfMessage == nil {
|
||||
t.Fatalf("expected one EasyInputMessage, got %+v", input)
|
||||
}
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "[U_alice] My name is Alice") {
|
||||
t.Errorf("expected prefixed content in payload, got: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTranslateMessages_UserNamePrefixedMultipart verifies the same
|
||||
// behavior on the multipart code path used when media is attached.
|
||||
func TestTranslateMessages_UserNamePrefixedMultipart(t *testing.T) {
|
||||
msgs := []protocoltypes.Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: "look at this",
|
||||
Name: "U_alice",
|
||||
Media: []string{"data:image/png;base64,abc"},
|
||||
},
|
||||
}
|
||||
input, _ := TranslateMessages(msgs)
|
||||
if len(input) != 1 || input[0].OfInputMessage == nil {
|
||||
t.Fatalf("expected InputMessage with multipart content, got %+v", input)
|
||||
}
|
||||
data, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "[U_alice] look at this") {
|
||||
t.Errorf("expected prefixed text part in payload, got: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTranslateMessages_ToolResultNotPrefixed protects against accidentally
|
||||
// prefixing tool result outputs (msg.ToolCallID set) — those are function
|
||||
// outputs, not user utterances.
|
||||
func TestTranslateMessages_ToolResultNotPrefixed(t *testing.T) {
|
||||
msgs := []protocoltypes.Message{
|
||||
{Role: "user", Content: `{"temp":72}`, ToolCallID: "call_1", Name: "alice"},
|
||||
}
|
||||
input, _ := TranslateMessages(msgs)
|
||||
if len(input) != 1 || input[0].OfFunctionCallOutput == nil {
|
||||
t.Fatalf("expected FunctionCallOutput, got %+v", input)
|
||||
}
|
||||
data, _ := json.Marshal(input)
|
||||
if strings.Contains(string(data), "[alice]") {
|
||||
t.Errorf("tool result must not carry sender prefix, got: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue