Skip to content

.NET: Preserve citations in OpenAI Responses hosting - #9813

Open
Roger Barreto (rogerbarreto) wants to merge 3 commits into
microsoft:mainfrom
rogerbarreto:openai-citation-preservation
Open

Roger Barreto (rogerbarreto) wants to merge 3 commits into
microsoft:mainfrom
rogerbarreto:openai-citation-preservation

Conversation

@rogerbarreto

@rogerbarreto Roger Barreto (rogerbarreto) commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Motivation & Context

OpenAI Responses hosting dropped citation annotations attached to TextContent. Completed responses and streaming output therefore omitted citations supplied by the agent. Preserve citation metadata in Responses API output so clients can access source links and text positions.

Description & Review Guide

  • What are the major changes? Convert supported URL, file, file path, and container file annotations using the OpenAI SDK wire representation. Emit streaming annotation events and include annotations in the final text part, message item, and completed response.
  • What is the impact of these changes? Responses clients receive citation metadata in completed and streaming output. No public API or dependency changes.
  • What do you want reviewers to focus on? Review citation serialization and streaming annotation indexes, including citations that arrive in a later update.

Related Issue

N/A - trivial change. Corrects an obvious omission: citations on text should remain available in OpenAI Responses output and stream events.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: AI assistance was used for implementation, regression tests, and this PR description. I reviewed and verified the submitted changes.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings October 28, 2026 22:48
@agent-framework-automation agent-framework-automation Bot added the .NET Usage: [Issues, PRs], Target: .Net label Oct 28, 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.

🟡 Changes recommended

Streaming drops annotation-only citation updates, while other paths can omit, misplace, or duplicate citations.

5 open issues
What changed in this PR

This PR adds citation preservation to .NET OpenAI Responses hosting so clients can receive source metadata in completed responses and streaming output.

Changes:

  • Converts URL, file, file-path, and container-file citations into OpenAI wire format.
  • Adds streaming annotation events and retains annotations in final response payloads.
  • Adds regression tests for citation metadata, event indexes, and later annotation updates.
File Description
dotnet/​tests/​Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/​ResponseCitationTests.cs Tests citation conversion and completed/streaming payloads.
dotnet/​src/​Microsoft.Agents.AI.Hosting.OpenAI/​Responses/​Streaming/​AssistantMessageEventGenerator.cs Emits annotation events and accumulates citations for completion.
dotnet/​src/​Microsoft.Agents.AI.Hosting.OpenAI/​Responses/​Models/​StreamingResponseEvent.cs Defines the annotation-added event model.
dotnet/​src/​Microsoft.Agents.AI.Hosting.OpenAI/​Responses/​Converters/​ItemContentConverter.cs Converts supported citations into serialized annotations.
dotnet/​src/​Microsoft.Agents.AI.Hosting.OpenAI/​OpenAIHostingJsonUtilities.cs Registers the new event for source-generated JSON serialization.

Review effort: Balanced

Delta = textContent.Text
};

foreach (JsonElement annotation in ItemContentConverter.ToAnnotations(textContent))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: route plain AIContent annotations to the active assistant item. Added same message and boundary regressions. Final part and completed response retain citations.

ItemContent? result = content switch
{
TextContent textContent => new ItemContentOutputText { Text = textContent.Text ?? string.Empty, Annotations = [], Logprobs = [] },
TextContent textContent => new ItemContentOutputText { Text = textContent.Text ?? string.Empty, Annotations = [.. ToAnnotations(textContent)], Logprobs = [] },

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: clone raw output text, merge current citations, preserve existing annotations and logprobs. Original raw item remains unchanged.

Comment on lines +175 to +192
if (citation.RawRepresentation is ResponseMessageAnnotation rawAnnotation)
{
yield return SerializeAnnotation(rawAnnotation);
continue;
}

if (citation.Url is not { } url || citation.AnnotatedRegions is not { } regions)
{
continue;
}

foreach (AnnotatedRegion region in regions)
{
if (region is TextSpanAnnotatedRegion { StartIndex: int start, EndIndex: int end })
{
yield return SerializeAnnotation(new UriCitationMessageAnnotation(url, start, end, citation.Title ?? string.Empty));
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: current structured URL, title, and regions take precedence. Raw URL remains fallback; raw file citations retain SDK metadata.

Delta = textContent.Text
};

foreach (JsonElement annotation in ItemContentConverter.ToAnnotations(textContent))

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: structured part local spans add preceding text length in UTF 16 units. SDK message wide annotations keep original offsets. Regression covers sibling text and supplementary Unicode.

Comment on lines +95 to +96
int annotationIndex = this._annotations.Count;
this._annotations.Add(annotation);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 6621045. Streaming now skips exact serialized annotation replays before assigning indexes; distinct spans and metadata remain.

@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.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): fe95d2478d7b
Model: gpt-5.6-sol

Overview

The change centralizes citation conversion through the OpenAI SDK wire representation, registers the new SSE event for source generation, and tests propagation through final and streaming response shapes. The conversion guards unsupported annotations and preserves raw OpenAI annotation metadata. Two residual gaps remain: the real MEAI OpenAI annotation-only streaming shape is not accepted, and combining multiple text parts can leave later citation spans pointing at the wrong text.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (1 high, 1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Streaming/AssistantMessageEventGenerator.cs

Delta = textContent.Text
};

foreach (JsonElement annotation in ItemContentConverter.ToAnnotations(textContent))

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.

MEAI OpenAI emits streaming citations after the text deltas as a base, annotation-only AIContent; the sibling Foundry converter handles that shape explicitly. This loop only reads annotations from TextContent, and the dispatcher completes the current item and skips the base AIContent, so the actual OpenAI streaming path still drops its citations. Please keep annotation-only updates on the current assistant item and feed their annotations into this accumulator, with a regression test using the provider's real update shape.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: route plain AIContent annotations to the active assistant item. Added same message and boundary regressions. Final part and completed response retain citations.

foreach (JsonElement annotation in ItemContentConverter.ToAnnotations(textContent))
{
int annotationIndex = this._annotations.Count;
this._annotations.Add(annotation);

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.

When one update contains multiple TextContent parts, this generator concatenates their text into a single output part but appends each citation's span unchanged. A citation local to a later part therefore points into the preceding text in the emitted annotation event and completed response. Please preserve separate output-text parts or offset annotations for distinct content parts while leaving provider-streaming annotations, whose indices are already message-relative, unchanged.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 95db2138: structured part local spans add preceding text length in UTF 16 units. SDK message wide annotations keep original offsets. Regression covers sibling text and supplementary Unicode.

@rogerbarreto

Roger Barreto (rogerbarreto) commented Oct 28, 2026 •

Copy link
Copy Markdown
Member Author

Assistant review assessment

Roger Barreto (@rogerbarreto), source inspection at fe95d2478d7be6f713a2ef67d751d86cc1970534 supports four findings that need your decision:

Review Evidence and impact Recommendation
Annotation-only content IsSupported accepts only TextContent; the dispatcher closes the active item and skips plain AIContent. Provider citations can disappear. Handle annotation-only content on the same active message and test the actual provider shape.
Original content bypass ToItemContent returns RawRepresentation before converting current annotations. Added citations can be lost. Reconcile citations while preserving original unrelated metadata.
Original URL precedence Raw annotation serialization returns before current URL, title and regions are inspected. Edits and extra spans are ignored. Prefer complete structured URL fields/regions with raw fallback; retain file handling.
Completed text parts Text is joined into one part but spans stay unchanged. A later citation can highlight earlier text. Preserve part boundaries or carefully adjust completed-part spans without shifting provider message-relative positions.

All fit this PR, but change content handling or metadata/position rules. Roger explicitly approved all four corrections through the app decision prompt. They are assigned to the existing PR coding session, with RED/GREEN coverage, affected Debug build/tests and scoped formatting required before publication. Multipart implementation must preserve provider message-relative positions; unsupported assumptions require a further decision.

The two GitHub Actions comments repeat the annotation-only and multipart findings. They are grouped, not separate tasks. Identical citation replay is a fifth, localized issue already routed to this PR's existing coding session under your minimal-fix authorization. Fixed in 662104583f7ee88e77eb416cf95c0452a59eee27, verified as the published PR head. Regression RED/GREEN, all-target Debug builds, 1,773 unit tests and scoped Windows formatting passed per the coding-session validation record. Docker formatting was not run; native Windows formatting was accepted. No thread resolution or merge is requested.

This branch was successfully deployed

1 active deployment
github-app-auth — 95db2138 Deployed Oct 28, 2026 by rogerbarreto via add_label #26463
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants