bug: OpenAI Responses streaming discards `output_text` annotations — hosted `web_search` citations never reach consumers
Description
When using a hosted search tool (web_search) with the OpenAI Responses API in streaming mode, the source citations OpenAI returns cannot be obtained through rig. The web_search_call item itself survives (thanks to #1546), but the annotations array carrying the actual source URLs is discarded.
The non-streaming path does preserve them, which suggests this is an oversight in the streaming decoder rather than an intentional omission.
Root cause
url_citation annotations live on the output_text content part:
{
"type": "message",
"id": "msg_1",
"role": "assistant",
"content": [{
"type": "output_text",
"text": "...",
"annotations": [{
"type": "url_citation",
"url": "https://example.com",
"title": "Example",
"start_index": 0,
"end_index": 7
}]
}]
}In streaming mode they are lost through three independent paths.
1. response.output_text.annotation.added is not handled at all.
annotation does not appear anywhere under rig-core/src/providers/openai/ (the only Annotation type in the crate is Gemini's, in interactions_api). The SSE dispatcher matches a fixed list of event types and falls through to a catch-all at rig-core/src/providers/openai/responses_api/streaming.rs:526:
_ => {}2. On response.output_item.done, the message's content parts are discarded.
streaming.rs:378-380, in push_output_item_done:
Output::Message(message) => {
immediate.push(streaming::RawStreamingChoice::MessageId(message.id));
}Only the id is emitted; OutputMessage.content — where the annotations are — is dropped.
3. On response.completed, the output array is discarded.
streaming.rs:320-331, record_response_chunk copies only usage and reasoning_metadata / reasoning_context.
Additionally, OutputTextChunk (streaming.rs:811) models only content_index, sequence_number, and text, so response.output_text.done cannot carry annotations through either.
The data does arrive — it is decoded and then thrown away
Text carries #[serde(flatten)] additional_params: Option<Value> (rig-core/src/completion/message.rs:350-356), which captures unknown fields. Deserializing the message above into OutputMessage yields:
OutputText(Text {
text: "hi",
additional_params: Some(Object {
"annotations": Array [Object {
"type": "url_citation",
"url": "https://example.com",
"title": "Example",
"start_index": 0,
"end_index": 7,
}],
}),
})and it round-trips on serialize. So the annotations are already sitting on AssistantContent::OutputText(Text) after decode — the streaming accumulator simply drops them.
Expected behavior
Streaming consumers should be able to obtain output_text annotations, ideally both incrementally (via response.output_text.annotation.added) and in the final aggregated response.
Suggested fix
At minimum, handle the response.output_text.annotation.added event type in the dispatcher and surface it — either as a dedicated ItemChunkKind / RawStreamingChoice pair, or by reusing the RawStreamingChoice::Unknown path that already carries web_search_call items verbatim via Output::Unknown (streaming.rs:382-387). Handling the event type is required either way, since today it never reaches the decoder.
Environment
- rig-core 0.41.0
- OpenAI Responses API with hosted
web_search - Also affects xAI, which reuses
responses_api::{Output, ResponsesUsage}and its streaming types (rig-core/src/providers/xai/completion.rs:16-18)
Related
- #1546 — same class of gap (hosted-tool streaming events silently dropped, breaking
usage). Fixed by addingOutput::Unknown; that fix explicitly noted that "adding explicit variants and exposing web search events via the streaming API would be a nice enhancement but is not required to fix the usage tracking regression".
Source: 0xPlaygrounds/rig