diff --git a/agent/response.go b/agent/response.go index 4a966e69..737054e6 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"` + // ConversationID identifies conversation history retained after this response // by the service or per-service-call history persistence. When nil, the response // does not claim that its messages can be recovered from a retained conversation. @@ -157,6 +161,7 @@ func (resp *Response) ToUpdates() []*ResponseUpdate { AgentID: resp.AgentID, MessageID: msg.ID, ResponseID: resp.ID, + ModelID: resp.ModelID, ConversationID: resp.ConversationID, FinishReason: resp.FinishReason, AuthorName: msg.AuthorName, @@ -166,11 +171,12 @@ func (resp *Response) ToUpdates() []*ResponseUpdate { }) } - if hasAdditionalProperties || resp.ContinuationToken != "" { + if hasAdditionalProperties || resp.ContinuationToken != "" || resp.ModelID != "" { extra := &ResponseUpdate{ AdditionalProperties: resp.AdditionalProperties, AgentID: resp.AgentID, ResponseID: resp.ID, + ModelID: resp.ModelID, ConversationID: resp.ConversationID, ContinuationToken: resp.ContinuationToken, CreatedAt: resp.CreatedAt, @@ -213,6 +219,7 @@ func (resp *Response) Update(update *ResponseUpdate) { // clear values already received for the response. resp.AgentID = cmp.Or(update.AgentID, resp.AgentID) resp.ID = cmp.Or(update.ResponseID, resp.ID) + resp.ModelID = cmp.Or(update.ModelID, resp.ModelID) if update.ConversationID != nil { resp.ConversationID = update.ConversationID } @@ -310,6 +317,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"` + // ConversationID identifies history retained by the service or per-service-call // history persistence. Providers set it only when later requests can refer to that // history instead of resending the messages. Nil means this update does not diff --git a/agent/response_test.go b/agent/response_test.go index 1031181a..e9bb33bc 100644 --- a/agent/response_test.go +++ b/agent/response_test.go @@ -486,6 +486,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 a819a965..dea41008 100644 --- a/provider/openaiprovider/chat_test.go +++ b/provider/openaiprovider/chat_test.go @@ -592,6 +592,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 a6084178..84c01dde 100644 --- a/provider/openaiprovider/responses.go +++ b/provider/openaiprovider/responses.go @@ -1120,6 +1120,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, @@ -1141,6 +1142,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 != "" {