Citations and sources
Overview
Every grounded answer from calypso-rag-agent carries structured citation data so your app can render inline references and a source list without parsing the answer text. This page is the canonical schema reference for that data on both endpoints.
Calypso follows the OpenAI annotation model: citations are annotation objects anchored to the generated text by character offsets. The answer text stays plain; your client maps start_index / end_index spans back onto the text and renders [1]-style markers, links, or hover cards.
| Surface | Where citations live | Where sources live |
|---|---|---|
| Responses | output[] → message → output_text.annotations[] | output[] → file_search_call.results[] (plus metadata._aicore in legacy mode) |
| Chat Completions | choices[0].message.annotations[] | Plain-text "Sources:" appendix in the message content |
Prefer the Responses endpoint for citation-aware UIs: it exposes the full structured surface. Chat Completions is limited to OpenAI's nested url_citation annotation shape.
Responses: response anatomy
A grounded, non-streaming Responses reply contains two output items — the file search call that retrieved the evidence, and the assistant message with annotated text:
{
"id": "resp_abc123",
"object": "response",
"model": "calypso-rag-agent",
"output": [
{
"type": "file_search_call",
"status": "completed",
"results": [
{
"file_id": "file-1",
"filename": "refund_policy_2026.pdf",
"text": "Customers may request a full refund within 30 days of purchase.",
"attributes": {
"source_index": 1,
"label": "refund_policy_2026.pdf (p. 4)",
"page_number": 4,
"page_label": "4",
"locator_label": "p. 4",
"modality": "text",
"knowledge_id": "kb-1"
}
}
]
},
{
"type": "message",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Refunds are available within 30 days of purchase.",
"annotations": [
{
"type": "file_citation",
"text": "Refunds are available within 30 days of purchase.",
"start_index": 0,
"end_index": 49,
"file_id": "file-1",
"filename": "refund_policy_2026.pdf",
"index": 0,
"file_citation": {
"file_id": "file-1",
"filename": "refund_policy_2026.pdf",
"quote": "Customers may request a full refund within 30 days of purchase."
}
}
]
}
]
}
],
"metadata": {
"_aicore": {
"retrieval_status": "grounded",
"grounded_sources_state": "available",
"grounded_sources_hint": "Retrieved context used to support this answer."
}
}
}
Annotation objects
Each annotation in output_text.annotations[] carries the OpenAI-native flat fields at the top level, plus a legacy nested object kept for backward compatibility. Three types are emitted:
| Field | Type | Present on | Description |
|---|---|---|---|
type | string | all | url_citation, file_citation, or media_citation (Calypso extension for image/audio/video evidence) |
text | string | all | The exact answer segment the citation supports |
start_index / end_index | int | all | Character span of text within the output_text it belongs to |
url / title | string | url_citation | Flat OpenAI fields for web sources |
file_id / filename | string | file_citation | Flat OpenAI fields for file sources |
index | int | file_citation | OpenAI-standard insertion point (equals start_index) |
url_citation / file_citation / media_citation | object | matching type | Legacy nested shape; same data as the flat fields plus quote for files |
_aicore | object | all | Calypso extension: grounded_source (the full source descriptor), chunk_index, segment_text, segment_start, segment_end, support_indices |
The annotation _aicore.segment_* fields let you rebuild evidence panels ("which sentence is backed by which source") from annotations alone — no response-level metadata required.
File search results
The file_search_call.results[] array lists every retrieved source the answer draws on. Each result carries file_id, filename, text (the quoted evidence), an optional relevance score, and an attributes object:
| Attribute | Description |
|---|---|
source_index | Stable 1-based source number — matches the [n] numbering used in citations |
label | Display-ready source label, e.g. refund_policy_2026.pdf (p. 4) |
page_number / page_label | Page location within the document, when known |
locator_label | Short human-readable locator, e.g. p. 4 |
modality / mime_type | Source modality (text, multimodal, ...) and MIME type |
source_type / knowledge_id | Bucket-level provenance identifiers |
media_id / media_status | Present for media sources; media_status reports preview resolution |
access_url | Signed URL for opening the source, when available |
excerpt / preview | Structured evidence excerpt and preview descriptors |
Use source_index and label to render your source list directly from the native output — they make the response self-describing without any vendor metadata.
Response metadata (metadata._aicore)
Every Responses reply includes a compact status block:
| Key | Description |
|---|---|
retrieval_status | Whether retrieval ran and grounded the answer (e.g. grounded) |
grounded_sources_state | available, hidden, missing, or none — see grounding states |
grounded_sources_hint | Display-ready caption for the sources panel |
In the default legacy mode, the block additionally duplicates the citation data as grounded_sources[] (indexed source descriptors), grounding_supports[] (per-segment evidence spans with support_index, chunks, text, start_index, end_index), display_text, and ui[]. This duplication exists for older consumers; new integrations should read the native surface and opt into native mode below.
Citation compatibility modes
Control how much duplicated metadata you receive with two request-level metadata keys (top-level or nested under metadata._aicore):
| Key | Values | Default | Effect |
|---|---|---|---|
citation_compat | legacy, native | legacy | native prunes the duplicated _aicore citation payload; annotations and file_search_call.results[].attributes become the canonical carrier |
sources_appendix | include, omit | include | omit returns the answer body without the trailing plain-text "Sources:" block |
resp = client.responses.create(
model="calypso-rag-agent",
input="What is our refund policy?",
metadata={
"citation_compat": "native",
"sources_appendix": "omit"
}
)
In native mode the response is leaner and fully OpenAI-shaped: sources come from file_search_call.results[] (source_index, label), inline citations from annotations[], and evidence spans from each annotation's _aicore.segment_* fields. The compact status keys (retrieval_status, grounded_sources_state, grounded_sources_hint) are always present in both modes.
grounded_sources / grounding_supports metadata, nothing changes until you opt in.Responses: streaming events
When stream=True, citation data arrives as typed SSE events alongside the text deltas:
| Event | Payload |
|---|---|
response.output_text.delta | Incremental answer text |
response.output_text.annotation.added | One event per citation annotation, with annotation_index and the full annotation object |
response.content_part.done / response.output_item.done | Final text with the complete annotations[] array |
response.completed | Full response object, including file_search_call results and metadata |
for event in stream:
if event.type == "response.output_text.delta":
render_text(event.delta)
elif event.type == "response.output_text.annotation.added":
render_citation(event.annotation)
Chat Completions: citations
Chat Completions follows OpenAI's spec for that surface, which only defines the nested url_citation annotation shape. Citations appear on choices[0].message.annotations[]:
{
"choices": [
{
"message": {
"role": "assistant",
"content": "Refunds are available within 30 days of purchase. [1]\n\nSources:\n[1] refund_policy_2026.pdf (p. 4)",
"annotations": [
{
"type": "url_citation",
"url_citation": {
"url": "https://files.calypso.so/signed/refund_policy_2026.pdf",
"title": "refund_policy_2026.pdf",
"start_index": 0,
"end_index": 49
}
}
]
}
}
]
}
Behavior to note:
- File and media sources are mapped into
url_citationannotations: the URL is a signed access URL when one is available, and the title falls back to the filename so you can always label the source. - The answer content includes a plain-text "Sources:" appendix, since this surface has no structured source list.
- When streaming, annotations arrive in a final
delta.annotationschunk just before thefinish_reasonchunk.
For structured file search results, grounding supports, and the native compat mode, use the Responses endpoint.
Rendering guidance
- Anchor by offsets, not by markers. Use each annotation's
start_index/end_indexto place inline references; don't parse[n]tokens out of the text. - Number sources with
source_index. It is stable, 1-based, and consistent with the citation order — use it for your[n]badges and source list. - Sort before slicing. If you decorate text by span, apply annotations in descending
start_indexorder so earlier insertions don't shift later offsets. - Respect the grounding state. Only render a sources panel when
grounded_sources_stateisavailable;hiddenmeans the workspace policy suppresses sources, andmissing/nonemean there is nothing to show. See grounding states.