From 8bd67c5252d8b2de162c0e5564982cb1c2ac94bd Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Thu, 16 Jul 2026 14:11:07 +0530 Subject: [PATCH 1/3] Keep AdditionalProperties on every Responses output message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the non-streaming Responses path, currentUpdate is reset to a fresh &ResponseUpdate{} for each output message after the first, then its MessageID/ResponseID/Role/etc. are repopulated — but AdditionalProperties (e.g. EndUserId from resp.User) was not, so the second and later messages lost it. Only the first message and the response-level merge retained it. Repopulate AdditionalProperties per message from the response so every message carries it. Adds a test with two output messages and a response-level user; the second message's update reports an empty EndUserId before this change and the correct value after. --- provider/openaiprovider/responses.go | 5 ++ .../responses_additionalprops_test.go | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 provider/openaiprovider/responses_additionalprops_test.go diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 1b712031..1dde4789 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -827,6 +827,11 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func currentUpdate.RawRepresentation = out currentUpdate.Role = message.Role(out.Role) currentUpdate.CreatedAt = time.Unix(int64(resp.CreatedAt), 0) + // Repopulate response-level properties: currentUpdate is reset to a + // fresh value for each output message after the first, which would + // otherwise drop AdditionalProperties (e.g. EndUserId) on the second + // and later messages. + currentUpdate.AdditionalProperties = responsesPopulateAdditionalProperties(resp) for _, c := range out.Content { switch c := c.AsAny().(type) { case responses.ResponseOutputText: diff --git a/provider/openaiprovider/responses_additionalprops_test.go b/provider/openaiprovider/responses_additionalprops_test.go new file mode 100644 index 00000000..d518533c --- /dev/null +++ b/provider/openaiprovider/responses_additionalprops_test.go @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft. All rights reserved. + +package openaiprovider_test + +import ( + "testing" +) + +// A non-streaming Responses result with more than one output message must carry +// response-level AdditionalProperties (e.g. EndUserId) on every message, not +// only the first. currentUpdate is reset to a fresh value for each message +// after the first, so the properties must be repopulated per message. +func TestResponses_NonStreaming_AllMessagesKeepAdditionalProperties(t *testing.T) { + const input = ` + { + "model":"gpt-4o-mini", + "input": [{ + "type":"message", + "role":"user", + "content":[{"type":"input_text","text":"hello"}] + }] + }` + const output = ` + { + "id":"resp_multi", + "object":"response", + "created_at":1741891428, + "status":"completed", + "error":null, + "incomplete_details":null, + "model":"gpt-4o-mini", + "user":"end-user-42", + "output":[ + {"type":"message","id":"msg_1","status":"completed","role":"assistant","content":[{"type":"output_text","text":"first","annotations":[]}]}, + {"type":"message","id":"msg_2","status":"completed","role":"assistant","content":[{"type":"output_text","text":"second","annotations":[]}]} + ] + }` + + server := newTestResponsesServer(t, input, output) + defer server.Close() + a := newTestResponsesClient(server, "gpt-4o-mini") + + var msgUpdates int + for u, err := range a.RunText(t.Context(), "hello") { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.MessageID == "" { + continue + } + msgUpdates++ + if got, _ := u.AdditionalProperties["EndUserId"].(string); got != "end-user-42" { + t.Errorf("message %q: EndUserId = %q, want %q", u.MessageID, got, "end-user-42") + } + } + if msgUpdates != 2 { + t.Fatalf("expected 2 message-bearing updates, got %d", msgUpdates) + } +} From 5d32df090d898deded4547739f98a8f936c271bf Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Thu, 16 Jul 2026 14:56:45 +0530 Subject: [PATCH 2/3] Repopulate Responses AdditionalProperties in the reset block Move the per-message AdditionalProperties repopulation into the message-transition reset so it only runs when currentUpdate is reset, rather than re-setting it on every message including the first. --- provider/openaiprovider/responses.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 1dde4789..092823c3 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -815,7 +815,12 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func if !yield(currentUpdate, nil) { return } - currentUpdate = &agent.ResponseUpdate{} + // Reset for the next message, carrying the response-level + // properties forward so the second and later messages keep + // AdditionalProperties (e.g. EndUserId). + currentUpdate = &agent.ResponseUpdate{ + AdditionalProperties: responsesPopulateAdditionalProperties(resp), + } } currentUpdate.MessageID = out.ID currentUpdate.ResponseID = resp.ID @@ -827,11 +832,6 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func currentUpdate.RawRepresentation = out currentUpdate.Role = message.Role(out.Role) currentUpdate.CreatedAt = time.Unix(int64(resp.CreatedAt), 0) - // Repopulate response-level properties: currentUpdate is reset to a - // fresh value for each output message after the first, which would - // otherwise drop AdditionalProperties (e.g. EndUserId) on the second - // and later messages. - currentUpdate.AdditionalProperties = responsesPopulateAdditionalProperties(resp) for _, c := range out.Content { switch c := c.AsAny().(type) { case responses.ResponseOutputText: From dd7e77790b7654d9ac93c742e9579a8947d04e97 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Fri, 17 Jul 2026 15:22:25 +0530 Subject: [PATCH 3/3] Move Responses AdditionalProperties test into responses_test.go Address review feedback: consolidate into the package's canonical test file. --- .../responses_additionalprops_test.go | 59 ------------------- provider/openaiprovider/responses_test.go | 52 ++++++++++++++++ 2 files changed, 52 insertions(+), 59 deletions(-) delete mode 100644 provider/openaiprovider/responses_additionalprops_test.go diff --git a/provider/openaiprovider/responses_additionalprops_test.go b/provider/openaiprovider/responses_additionalprops_test.go deleted file mode 100644 index d518533c..00000000 --- a/provider/openaiprovider/responses_additionalprops_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -package openaiprovider_test - -import ( - "testing" -) - -// A non-streaming Responses result with more than one output message must carry -// response-level AdditionalProperties (e.g. EndUserId) on every message, not -// only the first. currentUpdate is reset to a fresh value for each message -// after the first, so the properties must be repopulated per message. -func TestResponses_NonStreaming_AllMessagesKeepAdditionalProperties(t *testing.T) { - const input = ` - { - "model":"gpt-4o-mini", - "input": [{ - "type":"message", - "role":"user", - "content":[{"type":"input_text","text":"hello"}] - }] - }` - const output = ` - { - "id":"resp_multi", - "object":"response", - "created_at":1741891428, - "status":"completed", - "error":null, - "incomplete_details":null, - "model":"gpt-4o-mini", - "user":"end-user-42", - "output":[ - {"type":"message","id":"msg_1","status":"completed","role":"assistant","content":[{"type":"output_text","text":"first","annotations":[]}]}, - {"type":"message","id":"msg_2","status":"completed","role":"assistant","content":[{"type":"output_text","text":"second","annotations":[]}]} - ] - }` - - server := newTestResponsesServer(t, input, output) - defer server.Close() - a := newTestResponsesClient(server, "gpt-4o-mini") - - var msgUpdates int - for u, err := range a.RunText(t.Context(), "hello") { - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if u.MessageID == "" { - continue - } - msgUpdates++ - if got, _ := u.AdditionalProperties["EndUserId"].(string); got != "end-user-42" { - t.Errorf("message %q: EndUserId = %q, want %q", u.MessageID, got, "end-user-42") - } - } - if msgUpdates != 2 { - t.Fatalf("expected 2 message-bearing updates, got %d", msgUpdates) - } -} diff --git a/provider/openaiprovider/responses_test.go b/provider/openaiprovider/responses_test.go index 4f060310..fd32592e 100644 --- a/provider/openaiprovider/responses_test.go +++ b/provider/openaiprovider/responses_test.go @@ -5546,3 +5546,55 @@ func responsesBodyEqual(t *testing.T, got string, want string) { t.Errorf("body\ngot %s\nwant %s", gotOut, wantOut) } } + +// A non-streaming Responses result with more than one output message must carry +// response-level AdditionalProperties (e.g. EndUserId) on every message, not +// only the first. currentUpdate is reset to a fresh value for each message +// after the first, so the properties must be repopulated per message. +func TestResponses_NonStreaming_AllMessagesKeepAdditionalProperties(t *testing.T) { + const input = ` + { + "model":"gpt-4o-mini", + "input": [{ + "type":"message", + "role":"user", + "content":[{"type":"input_text","text":"hello"}] + }] + }` + const output = ` + { + "id":"resp_multi", + "object":"response", + "created_at":1741891428, + "status":"completed", + "error":null, + "incomplete_details":null, + "model":"gpt-4o-mini", + "user":"end-user-42", + "output":[ + {"type":"message","id":"msg_1","status":"completed","role":"assistant","content":[{"type":"output_text","text":"first","annotations":[]}]}, + {"type":"message","id":"msg_2","status":"completed","role":"assistant","content":[{"type":"output_text","text":"second","annotations":[]}]} + ] + }` + + server := newTestResponsesServer(t, input, output) + defer server.Close() + a := newTestResponsesClient(server, "gpt-4o-mini") + + var msgUpdates int + for u, err := range a.RunText(t.Context(), "hello") { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if u.MessageID == "" { + continue + } + msgUpdates++ + if got, _ := u.AdditionalProperties["EndUserId"].(string); got != "end-user-42" { + t.Errorf("message %q: EndUserId = %q, want %q", u.MessageID, got, "end-user-42") + } + } + if msgUpdates != 2 { + t.Fatalf("expected 2 message-bearing updates, got %d", msgUpdates) + } +}