From 01a5b62042bcde8d919895bcf5656861ed22018f Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Tue, 22 Sep 2026 10:52:45 +0530 Subject: [PATCH 1/2] Add Response.ModelID and fold it from updates Response and ResponseUpdate now carry ModelID, the identifier of the model that produced the response, matching M.E.AI ChatResponse/ChatResponseUpdate (ProcessUpdate folds ModelId). Response.Update folds it like ResponseID, and ToUpdates round-trips it. The OpenAI (chat + responses), Anthropic, and Gemini providers populate it from the model on the response. ConversationID is intentionally not added: in Go the conversation identity lives on Session.ServiceID, so a separate field would be redundant. Implements #1140. --- agent/response.go | 11 +++++++++++ agent/response_test.go | 20 ++++++++++++++++++++ provider/anthropicprovider/agent.go | 1 + provider/geminiprovider/agent.go | 1 + provider/openaiprovider/chat.go | 2 ++ provider/openaiprovider/chat_test.go | 17 +++++++++++++++++ provider/openaiprovider/responses.go | 2 ++ 7 files changed, 54 insertions(+) diff --git a/agent/response.go b/agent/response.go index 0a74bedd..aaecb2bc 100644 --- a/agent/response.go +++ b/agent/response.go @@ -45,6 +45,10 @@ type Response struct { // ID identifies this response. ID string `json:",omitzero"` + // ModelID is the identifier of the model that produced this response, when + // the provider supplies it. It is empty otherwise. + ModelID string `json:",omitzero"` + // CreatedAt is the timestamp for the response. It is zero when the provider // did not supply a creation time. CreatedAt time.Time `json:",omitzero"` @@ -152,6 +156,7 @@ func (resp *Response) ToUpdates() []*ResponseUpdate { AgentID: resp.AgentID, MessageID: msg.ID, ResponseID: resp.ID, + ModelID: resp.ModelID, FinishReason: resp.FinishReason, AuthorName: msg.AuthorName, Role: msg.Role, @@ -203,6 +208,7 @@ func (resp *Response) Update(update *ResponseUpdate) { // Update the response object with those, preferring the values from later updates. resp.AgentID = cmp.Or(update.AgentID, resp.AgentID) resp.ID = cmp.Or(update.ResponseID, resp.ID) + resp.ModelID = cmp.Or(update.ModelID, resp.ModelID) resp.FinishReason = cmp.Or(update.FinishReason, resp.FinishReason) resp.RawRepresentation = appendRawRepresentation(resp.RawRepresentation, update.RawRepresentation) if update.ContinuationToken == "" { @@ -295,6 +301,11 @@ type ResponseUpdate struct { // ResponseID identifies the response of which this update is a part. ResponseID string + // ModelID is the identifier of the model that produced this update, when the + // provider supplies it. It is typically set on updates that carry provider + // response metadata. + ModelID string `json:",omitzero"` + // FinishReason is the reason the generation ended. It is typically set only // on the final update of a stream. Common values are "stop", "length", and // "tool_calls". diff --git a/agent/response_test.go b/agent/response_test.go index 7d45feee..a7dbe40d 100644 --- a/agent/response_test.go +++ b/agent/response_test.go @@ -455,6 +455,26 @@ func TestResponse_CreatedAt(t *testing.T) { } } +// ModelID folds onto the response from later updates and round-trips through +// ToUpdates, matching how ResponseID/FinishReason are handled. +func TestResponse_Update_ModelID(t *testing.T) { + resp := &agent.Response{} + resp.Update(&agent.ResponseUpdate{MessageID: "m1", Contents: message.Contents{&message.TextContent{Text: "hi"}}}) + resp.Update(&agent.ResponseUpdate{MessageID: "m1", ModelID: "gpt-4o-mini-2024-07-18"}) + if resp.ModelID != "gpt-4o-mini-2024-07-18" { + t.Fatalf("ModelID = %q, want gpt-4o-mini-2024-07-18", resp.ModelID) + } + + // Round-trip: ToUpdates carries ModelID, and re-collecting preserves it. + var collected agent.Response + for _, u := range resp.ToUpdates() { + collected.Update(u) + } + if collected.ModelID != resp.ModelID { + t.Errorf("round-tripped ModelID = %q, want %q", collected.ModelID, resp.ModelID) + } +} + func TestResponse_Update_AdditionalProperties(t *testing.T) { resp := &agent.Response{} diff --git a/provider/anthropicprovider/agent.go b/provider/anthropicprovider/agent.go index 43d25728..909374fd 100644 --- a/provider/anthropicprovider/agent.go +++ b/provider/anthropicprovider/agent.go @@ -124,6 +124,7 @@ func (a *client) run(ctx context.Context, messages []*message.Message, options . Role: message.RoleAssistant, MessageID: resp.ID, ResponseID: resp.ID, + ModelID: string(resp.Model), CreatedAt: time.Now(), FinishReason: mapStopReason(resp.StopReason), RawRepresentation: resp, diff --git a/provider/geminiprovider/agent.go b/provider/geminiprovider/agent.go index f56fa601..9aae1367 100644 --- a/provider/geminiprovider/agent.go +++ b/provider/geminiprovider/agent.go @@ -140,6 +140,7 @@ func (a *client) run(ctx context.Context, messages []*message.Message, options . yield(&agent.ResponseUpdate{ Contents: responseContents, Role: message.RoleAssistant, + ModelID: resp.ModelVersion, FinishReason: finishReason, CreatedAt: time.Now(), RawRepresentation: resp, diff --git a/provider/openaiprovider/chat.go b/provider/openaiprovider/chat.go index 30386aba..9c3b6f5e 100644 --- a/provider/openaiprovider/chat.go +++ b/provider/openaiprovider/chat.go @@ -160,6 +160,7 @@ func (a *chatClient) run(ctx context.Context, messages []*message.Message, optio Role: message.RoleAssistant, ResponseID: resp.ID, MessageID: resp.ID, + ModelID: resp.Model, FinishReason: finishReason, CreatedAt: time.Unix(resp.Created, 0), RawRepresentation: resp, @@ -216,6 +217,7 @@ func (a *chatClient) run(ctx context.Context, messages []*message.Message, optio Role: role, ResponseID: chunk.ID, MessageID: chunk.ID, + ModelID: chunk.Model, FinishReason: finishReason, CreatedAt: time.Unix(chunk.Created, 0), RawRepresentation: chunk, diff --git a/provider/openaiprovider/chat_test.go b/provider/openaiprovider/chat_test.go index 79fd8ac2..9962c721 100644 --- a/provider/openaiprovider/chat_test.go +++ b/provider/openaiprovider/chat_test.go @@ -475,6 +475,23 @@ func TestChatLegacyFunctionCallFinishReasonNormalized_Streaming(t *testing.T) { } } +// The model that produced the response must be surfaced on Response.ModelID. +func TestChatModelIDSurfaced(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"id":"chatcmpl-m","object":"chat.completion","created":1727888631,"model":"gpt-4o-mini-2024-07-18","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}]}`) + })) + defer server.Close() + + resp, err := newTestClient(server).RunText(t.Context(), "hi").Collect() + if err != nil { + t.Fatalf("error = %v", err) + } + if resp.ModelID != "gpt-4o-mini-2024-07-18" { + t.Errorf("ModelID = %q, want gpt-4o-mini-2024-07-18", resp.ModelID) + } +} + func TestChatURLCitationAnnotations_NonStreaming(t *testing.T) { const input = ` { diff --git a/provider/openaiprovider/responses.go b/provider/openaiprovider/responses.go index 0f380ffc..62dcc489 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -1080,6 +1080,7 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func currentUpdate := &agent.ResponseUpdate{ ResponseID: resp.ID, + ModelID: resp.Model, FinishReason: finishReason, CreatedAt: time.Unix(int64(resp.CreatedAt), 0), Role: message.RoleAssistant, @@ -1107,6 +1108,7 @@ func responsesProcessResponse(resp *responses.Response, seqNum int64, yield func } currentUpdate.MessageID = out.ID currentUpdate.ResponseID = resp.ID + currentUpdate.ModelID = resp.Model currentUpdate.FinishReason = finishReason // Only set ContinuationToken if it's not empty if contToken != "" { From 2b1d5cfdb60c768e3be0d265386b772a12c68317 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 3 Oct 2026 12:19:35 +0530 Subject: [PATCH 2/2] Round-trip ModelID through the metadata-only update in ToUpdates