Skip to content

[dotnet-port-api] Add Response.ModelID - #1156

Open
PratikDhanave (PratikDhanave) wants to merge 6 commits into
microsoft:mainfrom
PratikDhanaveFork:feat/response-modelid
Open

PratikDhanave (PratikDhanave) wants to merge 6 commits into
microsoft:mainfrom
PratikDhanaveFork:feat/response-modelid

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Implements #1140.

M.E.AI ChatResponse/ChatResponseUpdate carry a ModelId that ProcessUpdate folds onto the response. Go had no canonical, provider-agnostic way to read which model produced a response — it was only surfaced ad hoc via provider AdditionalProperties.

Change

  • Add ModelID to agent.Response and agent.ResponseUpdate.
  • Fold it in Response.Update (like ResponseID/FinishReason) and propagate it in ToUpdates.
  • Populate it from the model on the response in the OpenAI (chat + responses), Anthropic, and Gemini providers.

ConversationID from the issue is intentionally omitted: Go conversation identity lives on Session.ServiceID, so a separate field would be redundant.

Tests

  • TestResponse_Update_ModelID: ModelID folds from a later update and round-trips through ToUpdates.
  • TestChatModelIDSurfaced: the OpenAI chat provider surfaces the response model on Response.ModelID.

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 microsoft#1140.
Copilot AI lite review requested due to automatic review settings September 22, 2026 05:22
@github-actions github-actions Bot added area:agent Changes files in the agent area area:provider Changes files in the provider area area:provider/anthropic Changes files in the provider / anthropic area area:provider/gemini Changes files in the provider / gemini area area:provider/openai Changes files in the provider / openai area size:large At most 300 changed lines across at most 10 files labels Sep 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Streaming model IDs and metadata-only round-tripping remain incomplete.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 Medium severity

Open (4)
What changed in this PR

Adds provider-agnostic ModelID fields to agent responses and response updates, with provider mappings and folding support.

Changes:

  • Adds and propagates ModelID.
  • Surfaces model IDs across OpenAI, Anthropic, and Gemini providers.
  • Adds response and OpenAI chat tests.
File Reviewed changes
provider/​openaiprovider/​responses.go Non-streaming model ID is populated; streaming updates still omit it.
provider/​openaiprovider/​chat.go Surfaces the model ID for chat responses.
provider/​openaiprovider/​chat_test.go Tests OpenAI chat model ID exposure.
provider/​geminiprovider/​agent.go Non-streaming model ID is populated; streaming updates still omit it.
provider/​anthropicprovider/​agent.go Non-streaming model ID is populated; streaming updates still omit it.
agent/​response.go Adds and folds ModelID; metadata-only updates omit it during round-tripping.
agent/​response_test.go Tests model ID folding and round-tripping.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agent/response.go
AgentID: resp.AgentID,
MessageID: msg.ID,
ResponseID: resp.ID,
ModelID: resp.ModelID,
Role: message.RoleAssistant,
MessageID: resp.ID,
ResponseID: resp.ID,
ModelID: string(resp.Model),
yield(&agent.ResponseUpdate{
Contents: responseContents,
Role: message.RoleAssistant,
ModelID: resp.ModelVersion,
Comment thread provider/openaiprovider/responses.go Outdated

currentUpdate := &agent.ResponseUpdate{
ResponseID: resp.ID,
ModelID: resp.Model,
@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure labels Sep 22, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the public-api-change Pull Request changes public APIs label Sep 22, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by Go API Consistency Review Agent · copilot · auto · 98.2 AIC · ⌖ 5.96 AIC · ⊞ 9.2K

Comment thread agent/response.go

// ModelID is the identifier of the model that produced this response, when
// the provider supplies it. It is empty otherwise.
ModelID string `json:",omitzero"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR adds ModelID to agent.Response/agent.ResponseUpdate (the Go equivalent of upstream AgentResponse/AgentResponseUpdate, not ChatResponse/ChatResponseUpdate). Upstream, ModelId/model exists only on the lower-level ChatResponse/ChatResponseUpdate types:

  • .NET: Microsoft.Agents.AI.Abstractions/AgentResponse.cs and AgentResponseUpdate.cs have no ModelId property (properties are AgentId, ResponseId, ContinuationToken, CreatedAt, FinishReason, Usage, RawRepresentation, AdditionalProperties). ModelId is folded only in AIAgentChatClient.CloneWithConversationId on ChatResponse (dotnet/src/Microsoft.Agents.AI/ChatClient/AIAgentChatClient.cs:368), and referenced in AgentResponseUpdateTests.ConstructorWithChatResponseUpdateRoundtrips (dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseUpdateTests.cs:42) as a ChatResponseUpdate field that is not asserted to roundtrip onto AgentResponseUpdate.
  • Python: AgentResponse.__init__/AgentResponseUpdate.__init__ (python/packages/core/agent_framework/_types.py:2848 and :3134) take no model/model_id parameter, while the sibling ChatResponse.__init__/ChatResponseUpdate.__init__ (same file, :2449 and :2735) do.

So both upstream implementations deliberately keep model identity at the chat-client layer and omit it from the agent-level response contract. Adding ModelID unconditionally to Go's agent.Response/ResponseUpdate (the AgentResponse analog) is a divergence from that design, not merely an idiomatic Go difference — it's a new agent-level field neither upstream implementation exposes.

Suggested resolution: either (a) confirm with maintainers this is an intentional Go-specific enhancement over the agent-level contract (and document that divergence), or (b) if a ModelID surface is wanted, scope it to a chat-level type analogous to ChatResponse/ChatResponseUpdate if/when Go introduces one, rather than agent.Response.

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix review comments.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Firewall blocked 3 domains

The following domains were blocked by the firewall during workflow execution:

  • proxy.golang.org
  • storage.googleapis.com
  • sum.golang.org

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "proxy.golang.org"
    - "storage.googleapis.com"
    - "sum.golang.org"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent for #1156 · copilot · auto · 125.5 AIC · ⌖ 7.69 AIC · ⊞ 13.9K

Comment thread agent/response.go

// ModelID is the identifier of the model that produced this response, when
// the provider supplies it. It is empty otherwise.
ModelID string `json:",omitzero"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity concern (unresolved from prior review): ModelID added at the agent-response layer, not the chat-response layer

Upstream .NET and Python deliberately keep model identity on the lower-level chat-response contract and omit it from the agent-level response contract that Go's agent.Response/ResponseUpdate mirror:

  • .NET: Microsoft.Agents.AI.Abstractions/AgentResponse.cs and AgentResponseUpdate.cs have no ModelId property (members are AgentId, ResponseId, ContinuationToken, CreatedAt, FinishReason, Usage, RawRepresentation, AdditionalProperties). ModelId is folded only onto ChatResponse in AIAgentChatClient.CloneWithConversationId (dotnet/src/Microsoft.Agents.AI/ChatClient/AIAgentChatClient.cs:368). AgentResponseUpdateTests.ConstructorWithChatResponseUpdateRoundtrips (dotnet/tests/Microsoft.Agents.AI.Abstractions.UnitTests/AgentResponseUpdateTests.cs:42) exercises ModelId as a ChatResponseUpdate fixture field but never asserts it round-trips onto AgentResponseUpdate.
  • Python: AgentResponse.__init__/AgentResponseUpdate.__init__ (python/packages/core/agent_framework/_types.py:2848, :3134) take no model/model_id parameter, while sibling ChatResponse.__init__/ChatResponseUpdate.__init__ (same file, :2449, :2735) do.

Go has no chat-level response type distinct from agent.Response, so this PR's choice to add ModelID at the agent layer is a real API-shape divergence from both upstream implementations, not just an idiomatic naming difference. Suggest either explicitly confirming with maintainers that this is an intentional Go-specific widening of the agent-level contract (and documenting the divergence in the doc comment), or deferring the field to a future chat-level type if/when Go introduces one.


currentUpdate := &agent.ResponseUpdate{
ResponseID: resp.ID,
ModelID: resp.Model,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ModelID is only propagated on non-streaming responses across three providers

This PR populates ResponseUpdate.ModelID from the final/non-streaming response object in responsesProcessResponse (here), provider/anthropicprovider/agent.go (non-streaming branch, resp.Model), and provider/geminiprovider/agent.go (non-streaming branch, resp.ModelVersion). None of the corresponding streaming paths set it:

  • responsesProcessStreamingUpdate (provider/openaiprovider/responses.go) builds every streaming agent.ResponseUpdate via createUpdate, which never sets ModelID, even though ResponseCreatedEvent/ResponseCompletedEvent carry event.Response.Model.
  • The Anthropic streaming loop (provider/anthropicprovider/agent.go, around the stream.Next() loop) never reads accumulated.Model or event.Message.Model into the yielded updates.
  • The Gemini streaming loop (provider/geminiprovider/agent.go, around the per-chunk yield) never reads resp.ModelVersion into the yielded updates.

Upstream M.E.AI sets ChatResponseUpdate.ModelId consistently for both streaming and non-streaming chat responses (the same ModelId property backs both ChatResponse and ChatResponseUpdate, and provider adapters populate it per-chunk). As written, collecting a streaming run via ResponseStream.Collect on any of these three providers silently produces an empty Response.ModelID, even though the PR's non-streaming tests pass. Suggest populating ModelID in each streaming update from the per-chunk/per-event model field, matching the non-streaming branches added in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — ModelID is now set on the streaming updates for all three providers: OpenAI Responses threads the model through responsesStreamState from the lifecycle events' Response.Model; Anthropic reads it from the message_start Message.Model; Gemini reads each chunk's ModelVersion (and the final usage update). Added TestStreamingModelIDSurfaced to lock in the Collect() round-trip.

@github-actions

This comment has been minimized.

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Oct 8, 2026
@qmuntal
Quim Muntal (qmuntal) requested a balanced review from Copilot October 8, 2026 07:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A model-only response round-trip creates an empty assistant message instead of preserving an empty message list.

6 open findings

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread agent/response.go
Comment on lines +48 to +50
// ModelID is the identifier of the model that produced this response, when
// the provider supplies it. It is empty otherwise.
ModelID string `json:",omitzero"`
Comment thread agent/response.go
}

if hasAdditionalProperties || resp.ContinuationToken != "" {
if hasAdditionalProperties || resp.ContinuationToken != "" || resp.ModelID != "" {
…pdate

Response.Update unconditionally called targetMessage, so folding a metadata-only
ResponseUpdate (response-level ModelID / continuation token with no
contents/role/author/message ID, as emitted by ToUpdates) appended an empty
assistant message. A Response{ModelID: ...} with no messages therefore did not
survive a ToUpdates -> Update round-trip (Messages went 0 -> 1).

Gate the message-folding on updateHasMessageData(update); response-level fields
(ModelID, ResponseID, ...) are still applied unconditionally. Add regression
tests for the metadata-only update and the no-messages ModelID round-trip.
@github-actions github-actions Bot removed the parity-approved Go API consistency review found no parity issues label Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

API consistency review

Result Scope
unable to determine public API, user-visible behavior

Changed Go contract

New exported ModelID field on agent.Response and agent.ResponseUpdate. It is folded in Response.Update, propagated in ToUpdates, and populated by the OpenAI (chat and responses), Anthropic and Gemini providers.

Upstream evidence reviewed

Implementation Source Contract checked
.NET dotnet/src/Microsoft.Agents.AI.Abstractions/AgentResponse.cs and AgentResponseUpdate.cs (main) Neither file defines a ModelId member.
Python python/packages/core/agent_framework/_types.py (main) A text scan for model_id found no matches. I could not confirm the equivalent field name or its folding semantics.

Assessment

The PR adds a Go-only field on the agent-level response types. The .NET AgentResponse and AgentResponseUpdate do not expose ModelId directly. The issue cites M.E.AI ChatResponse.ModelId, but I could not inspect the M.E.AI sources. GitHub code search was rate-limited and the files were too large to inspect fully. I could not find a high-confidence mismatch or confirm parity, so I am reporting no finding. A maintainer should check how Python's AgentResponse and ChatResponse handle the model identifier, and whether the .NET AgentResponse.RawRepresentation or ChatResponse.ModelId is the intended source. parity-approved is removed until this is confirmed. public-api-change stays because exported API was added.

Generated by Go API Consistency Review Agent · copilot · auto · 36.8 AIC · ⌖ 0.614 AIC · ⊞ 4.9K · ◷

@PratikDhanave

Copy link
Copy Markdown
Contributor Author

Thanks for the parity flag — I dug into the layering and I think Go's structure makes this a parity-preserving change rather than a divergence. Wanted to lay out the reasoning and get your call.

The .NET/Python split doesn't exist in Go. Upstream keeps ModelId/model on ChatResponse/ChatResponseUpdate and omits it from AgentResponse/AgentResponseUpdate. But the Go SDK has no ChatResponse/ChatResponseUpdate type — the only response contracts are agent.Response and agent.ResponseUpdate, and the model providers (openaiprovider, anthropicprovider, geminiprovider, copilotprovider, foundryprovider) construct agent.ResponseUpdate directly from the raw model field (resp.Model, resp.ModelVersion, string(resp.Model), …).

So agent.ResponseUpdate is Go's chat-response-layer equivalent. It's the lowest-level, provider-emitted unit — the same role ChatResponseUpdate plays upstream. There's no lower contract to carry the served model id, so placing it here is the faithful Go analogue of where .NET/Python keep it, not a promotion to a higher layer.

If you'd prefer, I'm happy to:

  • document this explicitly on the ModelID field (noting it mirrors the upstream chat-layer ModelId), and/or
  • defer the change entirely if you'd rather model surface only via AdditionalProperties until Go grows a distinct chat-response type.

Separately, I've pushed fixes for the concrete review findings:

  • Round-trip bug: Response.Update no longer materializes an empty assistant message for a metadata-only update, so a Response{ModelID} with no messages now survives ToUpdates → Update (added regression tests).
  • Streaming ModelID propagation across the three providers and the ToUpdates metadata-only update were addressed earlier in the branch.

Let me know which direction you'd like on the layering and I'll finish accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Changes files in the agent area area:provider/anthropic Changes files in the provider / anthropic area area:provider/gemini Changes files in the provider / gemini area area:provider/openai Changes files in the provider / openai area area:provider Changes files in the provider area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure public-api-change Pull Request changes public APIs size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants