Skip to content

Surface web-search url_citation annotations on the OpenAI Chat Completions path like the Responses provider#700

Open
PratikDhanave wants to merge 2 commits into
microsoft:mainfrom
PratikDhanave:chat-url-citation-annotations
Open

Surface web-search url_citation annotations on the OpenAI Chat Completions path like the Responses provider#700
PratikDhanave wants to merge 2 commits into
microsoft:mainfrom
PratikDhanave:chat-url-citation-annotations

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

The OpenAI Chat Completions non-streaming conversion built TextContent only from choice.Message.Content and never read choice.Message.Annotations. As a result, the url_citation annotations that the web-search tool emits (the SDK's ChatCompletionMessageAnnotation with URLCitation{URL, Title, StartIndex, EndIndex}) were silently dropped, even though buildCompletionParams enables the web-search tool for *hostedtool.WebSearch.

This adds a small populateChatAnnotations helper that maps each url_citation onto the TextContent's Annotations as a *message.CitationAnnotation{URL, Title, RawRepresentation}, mirroring populateAnnotations on the Responses path.

Why

The Responses provider already surfaces web-search citations via populateAnnotations (ResponseOutputTextAnnotationURLCitation -> *message.CitationAnnotation) on both its streaming and non-streaming paths. The Chat Completions path was the only surface that discarded them, an inconsistency across the two OpenAI surfaces. The .NET and Python SDKs expose web-search citations on both the Chat Completions and Responses surfaces, so this restores cross-SDK and cross-surface parity.

Scope

Scoped to provider/openaiprovider/chat.go and its canonical test file. responses.go is intentionally untouched.

Testing

Added TestChatURLCitationAnnotations_NonStreaming in chat_test.go, a black-box test that drives a Chat Completion whose message carries a url_citation annotation through the existing httptest harness and asserts the emitted TextContent.Annotations contains a *message.CitationAnnotation with URL == https://example.com and Title == Example. The test fails before the change and passes after. go build ./..., go vet ./provider/openaiprovider/..., and go test ./provider/openaiprovider/... are green.

Copilot AI review requested due to automatic review settings July 24, 2026 02:11
@PratikDhanave
PratikDhanave requested a review from a team as a code owner July 24, 2026 02:11
The Chat Completions non-streaming conversion built TextContent only from
choice.Message.Content and ignored choice.Message.Annotations, so the
url_citation entries emitted by the web-search tool were dropped. The
Responses provider already maps these via populateAnnotations, so citations
were surfaced there but not on the Chat Completions path.

Populate the TextContent's Annotations with message.CitationAnnotation for
each url_citation, mirroring the Responses path and the .NET/Python SDKs
which expose web-search citations on both surfaces.
@PratikDhanave
PratikDhanave force-pushed the chat-url-citation-annotations branch from e601174 to 21c552a Compare July 24, 2026 02:14

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.

Pull request overview

Restores parity between the OpenAI Chat Completions and Responses provider surfaces by propagating web-search url_citation annotations into emitted message.TextContent.Annotations on the non-streaming Chat Completions path.

Changes:

  • Populate TextContent.Annotations from choice.Message.Annotations for non-streaming Chat Completions responses.
  • Add a targeted non-streaming test that verifies url_citation annotations are surfaced as *message.CitationAnnotation.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
provider/openaiprovider/chat.go Adds populateChatAnnotations and wires it into the non-streaming Chat Completions message-to-content conversion.
provider/openaiprovider/chat_test.go Adds a regression test covering non-streaming url_citation annotation propagation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +511 to +515
for _, ann := range anns {
content.Annotations = append(content.Annotations, &message.CitationAnnotation{
URL: ann.URLCitation.URL,
Title: ann.URLCitation.Title,
RawRepresentation: ann,

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.

Good catch — fixed in 62a8607: populateChatAnnotations now skips any annotation that isn't a url_citation or has an empty URLCitation.URL, so no bogus empty citations are emitted. This mirrors the type-filtering the Responses path does in populateAnnotations. Added a chat_test.go case locking the empty-URL skip.

@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 Jul 24, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Parity Review — ✅ Approved

This PR is a parity fix, not a divergence. The change adds an unexported helper populateChatAnnotations in provider/openaiprovider/chat.go that maps url_citation entries from the OpenAI Chat Completions API onto TextContent.Annotations as *message.CitationAnnotation.

Cross-SDK parity

SDK Chat Completions citations Responses/Streaming citations
.NET AgentResponseExtensions.csToChoiceMessageAnnotationsCitationAnnotation OutputConverter.cs
Python ✅ (per PR description)
Go (before this PR) ❌ silently dropped populateAnnotations
Go (after this PR) populateChatAnnotations

API surface

No new exported Go identifiers were added. CitationAnnotation, TextContent, and its Annotations field all preexisted. This is a behavior fix to restore cross-surface consistency within the Go provider itself and parity with .NET and Python.

The public-api-change label is not warranted. The existing parity-approved label correctly reflects the outcome of this review.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • awmgmcpg

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

network:
  allowed:
    - defaults
    - "awmgmcpg"

See Network Configuration for more information.

Generated by Go API Consistency Review Agent · 34.5 AIC · ⌖ 5.6 AIC · ⊞ 5.9K ·

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

Labels

parity-approved Go API consistency review found no parity issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants