Repository navigation
.NET: Preserve citations in OpenAI Responses hosting - #9813
Roger Barreto (rogerbarreto) wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Streaming drops annotation-only citation updates, while other paths can omit, misplace, or duplicate citations.
5 open issues
Preserve annotation-only AIContent updates in dispatcher · New Reconcile converted text annotations with raw item annotations · New Prefer structured citation regions over raw URL annotations · New Rebase citation offsets when concatenating separate text parts · New Deduplicate repeated citations before assigning annotation indexes · New
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)) |
There was a problem hiding this comment.
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 = [] }, |
There was a problem hiding this comment.
Fixed in 95db2138: clone raw output text, merge current citations, preserve existing annotations and logprobs. Original raw item remains unchanged.
| 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)); | ||
| } | ||
| } |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
| int annotationIndex = this._annotations.Count; | ||
| this._annotations.Add(annotation); |
There was a problem hiding this comment.
Fixed in 6621045. Streaming now skips exact serialized annotation replays before assigning indexes; distinct spans and metadata remain.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Assistant review assessment Roger Barreto (@rogerbarreto), source inspection at
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 |


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
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
AI assistance details: AI assistance was used for implementation, regression tests, and this PR description. I reviewed and verified the submitted changes.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.