Skip to content

Latest commit

 

History

History
443 lines (346 loc) · 14.5 KB

File metadata and controls

443 lines (346 loc) · 14.5 KB

Citations

Citations link spans of an assistant response back to the sources that support them. Turn on enableCitations when you create or resume a session, then read the citations payload on assistant.message events to render footnotes, source lists, or inline links.

Warning

Citations are experimental. The option name, event payload, and provider coverage can change in a future release.

How citations work

Citations are produced by the model provider, not by the SDK. The flow has three parts:

  1. Your application supplies citable material, such as a document attachment or a tool result that carries source content.
  2. The runtime marks that material as citable on the wire when enableCitations is on. For Anthropic models, file attachments are sent as document blocks with citations enabled.
  3. The model returns citation metadata, and the runtime normalizes it into a provider-agnostic citations object on the final assistant.message event.

Provider support is limited. The provider field on each source records where the citation came from:

Provider value Meaning
anthropic Citation produced by an Anthropic (Claude) model response
openai Citation produced by an OpenAI model response
client Citation synthesized by the runtime from tool output

Note

Turning on enableCitations does not guarantee that a response contains citations. Models emit them only when the response is grounded in citable source material. Always treat the citations field as optional.

Enable citations on a session

Set the option on session create, and set it again on resume if you want citations after a restart.

TypeScript
const session = await client.createSession({
    onPermissionRequest: approveAll,
    enableCitations: true,
});

const resumed = await client.resumeSession(session.sessionId, {
    onPermissionRequest: approveAll,
    enableCitations: true,
});
Python
session = await client.create_session(
    on_permission_request=PermissionHandler.approve_all,
    enable_citations=True,
)

resumed = await client.resume_session(
    session.session_id,
    on_permission_request=PermissionHandler.approve_all,
    enable_citations=True,
)
Go
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
	OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
	EnableCitations:     copilot.Bool(true),
})

resumed, err := client.ResumeSession(ctx, session.SessionID, &copilot.ResumeSessionConfig{
	OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
	EnableCitations:     copilot.Bool(true),
})
.NET
var session = await client.CreateSessionAsync(new SessionConfig
{
    OnPermissionRequest = PermissionHandler.ApproveAll,
    EnableCitations = true,
});

var resumed = await client.ResumeSessionAsync(session.SessionId, new ResumeSessionConfig
{
    OnPermissionRequest = PermissionHandler.ApproveAll,
    EnableCitations = true,
});
Java
CopilotSession session = client
        .createSession(new SessionConfig()
                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
                .setEnableCitations(true))
        .get();

CopilotSession resumed = client
        .resumeSession(session.getSessionId(), new ResumeSessionConfig()
                .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
                .setEnableCitations(true))
        .get();
Rust
let session = client
    .create_session(
        SessionConfig::new()
            .approve_all_permissions()
            .with_enable_citations(true),
    )
    .await?;

let resumed = client
    .resume_session(
        ResumeSessionConfig::new(session.id().clone())
            .approve_all_permissions()
            .with_enable_citations(true),
    )
    .await?;

Read citations from assistant messages

Citations arrive on the final assistant.message event, not on assistant.message_delta events. Wait for the final message before you render source markers.

TypeScript
session.on((event) => {
    if (event.type !== "assistant.message" || !event.data.citations) {
        return;
    }

    const { sources, spans } = event.data.citations;
    const sourceById = new Map(sources.map((source) => [source.id, source]));

    for (const span of spans) {
        const quoted = event.data.content.slice(span.startIndex, span.endIndex);
        for (const reference of span.references) {
            const source = sourceById.get(reference.sourceId);
            const label = source?.title ?? source?.url ?? source?.path ?? source?.id;
            console.log(`"${quoted}" — ${label}`);
        }
    }
});
Python
from copilot.session_events import SessionEventType

def utf16_slice(text: str, start: int, end: int) -> str:
    """Slice by UTF-16 code units, which is how span offsets are measured."""
    units = text.encode("utf-16-le")
    return units[start * 2 : end * 2].decode("utf-16-le")

def handle(event):
    if event.type != SessionEventType.ASSISTANT_MESSAGE or not event.data.citations:
        return

    sources = {source.id: source for source in event.data.citations.sources}

    for span in event.data.citations.spans:
        quoted = utf16_slice(event.data.content, span.start_index, span.end_index)
        for reference in span.references:
            source = sources[reference.source_id]
            label = source.title or source.url or source.path or source.id
            print(f'"{quoted}" — {label}')

session.on(handle)
Go
// import "unicode/utf16"

session.On(func(event copilot.SessionEvent) {
	d, ok := event.Data.(*copilot.AssistantMessageData)
	if !ok || d.Citations == nil {
		return
	}

	sources := map[string]copilot.CitationSource{}
	for _, source := range d.Citations.Sources {
		sources[source.ID] = source
	}

	// Span offsets are UTF-16 code units, so index the UTF-16 view of the content.
	units := utf16.Encode([]rune(d.Content))

	for _, span := range d.Citations.Spans {
		quoted := string(utf16.Decode(units[span.StartIndex:span.EndIndex]))
		for _, reference := range span.References {
			source := sources[reference.SourceID]
			label := source.ID
			switch {
			case source.Title != nil:
				label = *source.Title
			case source.URL != nil:
				label = *source.URL
			case source.Path != nil:
				label = *source.Path
			}
			fmt.Printf("%q — %s\n", quoted, label)
		}
	}
})
.NET
session.On<SessionEvent>(evt =>
{
    if (evt is not AssistantMessageEvent message || message.Data.Citations is null)
    {
        return;
    }

    var sources = message.Data.Citations.Sources.ToDictionary(source => source.Id);

    foreach (var span in message.Data.Citations.Spans)
    {
        var quoted = message.Data.Content[(int)span.StartIndex..(int)span.EndIndex];
        foreach (var reference in span.References)
        {
            var source = sources[reference.SourceId];
            var label = source.Title ?? source.Url ?? source.Path ?? source.Id;
            Console.WriteLine($"\"{quoted}\"{label}");
        }
    }
});
Java
session.on(AssistantMessageEvent.class, event -> {
    Citations citations = event.getData().citations();
    if (citations == null) {
        return;
    }

    Map<String, CitationSource> sources = citations.sources().stream()
            .collect(Collectors.toMap(CitationSource::id, source -> source));

    for (CitationSpan span : citations.spans()) {
        String quoted = event.getData().content()
                .substring(span.startIndex().intValue(), span.endIndex().intValue());
        for (CitationReference reference : span.references()) {
            CitationSource source = sources.get(reference.sourceId());
            String label = source.title() != null ? source.title()
                    : source.url() != null ? source.url()
                    : source.path() != null ? source.path()
                    : source.id();
            System.out.printf("\"%s\" — %s%n", quoted, label);
        }
    }
});
Rust
use github_copilot_sdk::session_events::AssistantMessageData;
use std::collections::HashMap;

let mut events = session.subscribe();

while let Ok(event) = events.recv().await {
    if event.event_type != "assistant.message" {
        continue;
    }

    let Some(data) = event.typed_data::<AssistantMessageData>() else {
        continue;
    };
    let Some(citations) = data.citations.as_ref() else {
        continue;
    };

    let sources: HashMap<&str, _> = citations
        .sources
        .iter()
        .map(|source| (source.id.as_str(), source))
        .collect();

    // Span offsets are UTF-16 code units, so index the UTF-16 view of the content.
    let units: Vec<u16> = data.content.encode_utf16().collect();

    for span in &citations.spans {
        let quoted = String::from_utf16_lossy(
            &units[span.start_index as usize..span.end_index as usize],
        );
        for reference in &span.references {
            let Some(source) = sources.get(reference.source_id.as_str()) else {
                continue;
            };
            let label = source
                .title
                .as_deref()
                .or(source.url.as_deref())
                .or(source.path.as_deref())
                .unwrap_or(source.id.as_str());
            println!("\"{quoted}\" — {label}");
        }
    }
}

Citation payload reference

The citations object separates deduplicated sources from the spans that reference them, so a source cited five times appears once in sources.

Type Field Description
Citations sources Deduplicated set of sources referenced by the citation spans
Citations spans Spans of generated text annotated with their supporting sources
CitationSource id Stable, turn-scoped identifier referenced by CitationReference.sourceId
CitationSource provider System that produced the citation: anthropic, openai, or client
CitationSource title? Human-readable title of the source
CitationSource url? URL of the source, when it is a web resource
CitationSource path? File path relative to the agent workspace root, when the source is a file
CitationSpan startIndex Start offset in the final message content (UTF-16 code units, zero-based, inclusive)
CitationSpan endIndex End offset in the final message content (UTF-16 code units, zero-based, exclusive)
CitationSpan references The sources that support this span
CitationReference sourceId Identifier of the CitationSource this reference points to
CitationReference citedText? Exact text from the source that supports the span, when the model provides it
CitationReference location? Location within the source that supports the span
CitationReference providerMetadata? Provider-native correlation data, passed through opaquely

Tip

Span offsets are measured in UTF-16 code units against the final content string. TypeScript, Java, and .NET strings are already UTF-16, so you can slice them directly. Python strings are indexed by Unicode code point and Go and Rust strings are UTF-8, so convert the content to UTF-16 code units before slicing, as the examples above do.

Citation locations

CitationReference.location is a discriminated union keyed on type:

Location type Fields Use
char startIndex, endIndex Character range within the source text
page startPage, endPage Page range within a paginated document
block startBlock, endBlock Content-block range within a structured document

Provide citable sources

Citations need source material the model can attribute. There are two ways to supply it.

Attach documents to a message

When citations are enabled and the session uses an Anthropic provider, file attachments are sent as document blocks with citations turned on, so the model can cite passages from them.

await session.sendAndWait({
    prompt: "Summarize the attached PDF and cite the passages you used.",
    attachments: [
        {
            type: "blob",
            data: pdfBase64,
            displayName: "quarterly-report.pdf",
            mimeType: "application/pdf",
        },
    ],
});

See Image input for the attachment API and the file and blob attachment shapes.

Return citable sources from a tool

Tool results carry an experimental citableSources array. Each entry supplies content that the model can cite, along with an id and optional title, url, and path. These sources are persisted with the tool result, so they survive session resume, and citations built from them are tagged with the client provider.

Limitations

  • Citations are experimental in every SDK and are not covered by compatibility guarantees.
  • Coverage depends on the model provider. A session configured for a provider without citation support emits no citations payload.
  • Citations are only present on the final assistant.message event, so streaming consumers cannot render them mid-response.
  • Public code and IP-duplication citations are not part of this surface.

Further reading