Skip to main content

rig_core/providers/copilot/
mod.rs

1//! GitHub Copilot authentication, endpoint wires, and model identifiers.
2//!
3//! A [`Copilot`] client sends with an exchanged session token. To log in with
4//! a device code or refresh a GitHub token, pass an [`auth::Authenticator`] to
5//! [`Copilot::authenticate`], which resolves the session through the client's
6//! own transport.
7//! Completion routes are selected by model; all routes include editor-identity headers.
8//!
9//! ```no_run
10//! use rig_core::providers::copilot;
11//!
12//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
13//! let github = copilot::Copilot::from_env()?;
14//!
15//! let chat = github.completion(copilot::GPT_4O);
16//! let codex = github.completion(copilot::GPT_5_3_CODEX);
17//! let embeddings = github.embedding(copilot::TEXT_EMBEDDING_3_SMALL, None);
18//! # let _ = (chat, codex, embeddings);
19//! # Ok(())
20//! # }
21//! ```
22
23/// Device login, session-token exchange, refresh, and shared credential caching.
24///
25/// ```no_run
26/// use rig_core::providers::copilot::auth::{AuthSource, Authenticator, DeviceCodeHandler};
27///
28/// let auth = Authenticator::new(AuthSource::OAuth, None, None, DeviceCodeHandler::default(), true);
29/// ```
30pub mod auth;
31pub mod extension;
32pub mod model_listing;
33pub mod wire;
34
35pub use crate::client::copilot::Copilot;
36pub use wire::CopilotConfig;
37
38use crate::completion;
39use crate::providers::openai;
40use serde::{Deserialize, Serialize};
41
42/// The API root a token that names no endpoint of its own resolves against.
43pub(crate) const GITHUB_COPILOT_API_BASE_URL: &str = "https://api.githubcopilot.com";
44pub(crate) const EDITOR_PLUGIN_VERSION: &str = "copilot-chat/0.35.0";
45pub(crate) const USER_AGENT: &str = "GitHubCopilotChat/0.35.0";
46pub(crate) const EDITOR_VERSION: &str = "vscode/1.107.0";
47const API_VERSION: &str = "2025-04-01";
48
49/// Stable descriptor name reported on normalized Copilot responses.
50pub const PROVIDER_NAME: &str = "copilot";
51
52/// Conversation intent sent in the `openai-intent` request header.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum CopilotIntent {
56    #[default]
57    Panel,
58    Edits,
59}
60
61impl CopilotIntent {
62    pub fn as_header(self) -> &'static str {
63        match self {
64            CopilotIntent::Panel => "conversation-panel",
65            CopilotIntent::Edits => "conversation-edits",
66        }
67    }
68}
69
70/// `gpt-4`
71pub const GPT_4: &str = "gpt-4";
72/// `gpt-4o`
73pub const GPT_4O: &str = "gpt-4o";
74/// `gpt-4o-mini`
75pub const GPT_4O_MINI: &str = "gpt-4o-mini";
76/// `gpt-4.1`
77pub const GPT_4_1: &str = "gpt-4.1";
78/// `gpt-4.1-mini`
79pub const GPT_4_1_MINI: &str = "gpt-4.1-mini";
80/// `gpt-4.1-nano`
81pub const GPT_4_1_NANO: &str = "gpt-4.1-nano";
82/// `gpt-5.3-codex`
83pub const GPT_5_3_CODEX: &str = "gpt-5.3-codex";
84/// `gpt-5.1-codex`
85pub const GPT_5_1_CODEX: &str = "gpt-5.1-codex";
86/// `gpt-5.5`
87pub const GPT_5_5: &str = "gpt-5.5";
88/// `gpt-5.4`
89pub const GPT_5_4: &str = "gpt-5.4";
90/// `claude-sonnet-4` completion model (Anthropic, via Copilot)
91pub const CLAUDE_SONNET_4: &str = "claude-sonnet-4";
92/// `claude-sonnet-4.6`
93pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4.6";
94/// `claude-opus-4.6`
95pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4.6";
96/// `claude-opus-4.7`
97pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4.7";
98/// `claude-3.5-sonnet` completion model (Anthropic, via Copilot)
99pub const CLAUDE_3_5_SONNET: &str = "claude-3.5-sonnet";
100/// `gemini-3-flash-preview` completion model (Google, via Copilot)
101pub const GEMINI_3_FLASH: &str = "gemini-3-flash-preview";
102/// `gemini-3.1-pro-preview` completion model (Google, via Copilot)
103pub const GEMINI_3_1_PRO_FLASH: &str = "gemini-3.1-pro-preview";
104/// `gemini-2.0-flash-001` completion model (Google, via Copilot)
105pub const GEMINI_2_0_FLASH: &str = "gemini-2.0-flash-001";
106/// `o3-mini` reasoning model (OpenAI, via Copilot)
107pub const O3_MINI: &str = "o3-mini";
108/// `text-embedding-3-small`
109pub const TEXT_EMBEDDING_3_SMALL: &str = "text-embedding-3-small";
110/// `text-embedding-3-large`
111pub const TEXT_EMBEDDING_3_LARGE: &str = "text-embedding-3-large";
112/// `text-embedding-ada-002`
113pub const TEXT_EMBEDDING_ADA_002: &str = "text-embedding-ada-002";
114
115/// The encoding [`wire::Embeddings::with_encoding_format`] asks for. Copilot
116/// relays OpenAI's embeddings wire, so it is OpenAI's own type.
117pub use openai::EncodingFormat;
118
119/// Build required editor-identity headers, including credentials and request identity.
120pub(crate) fn default_headers(
121    api_key: &str,
122    initiator: &'static str,
123    has_vision: bool,
124    intent: CopilotIntent,
125) -> Vec<(&'static str, String)> {
126    let mut headers = vec![
127        (
128            http::header::AUTHORIZATION.as_str(),
129            format!("Bearer {api_key}"),
130        ),
131        ("copilot-integration-id", "vscode-chat".to_string()),
132        ("editor-version", EDITOR_VERSION.to_string()),
133        ("editor-plugin-version", EDITOR_PLUGIN_VERSION.to_string()),
134        ("user-agent", USER_AGENT.to_string()),
135        ("openai-intent", intent.as_header().to_string()),
136        ("x-github-api-version", API_VERSION.to_string()),
137        ("x-request-id", crate::id::generate()),
138        (
139            "x-vscode-user-agent-library-version",
140            "electron-fetch".to_string(),
141        ),
142        ("X-Initiator", initiator.to_string()),
143    ];
144
145    if has_vision {
146        headers.push(("copilot-vision-request", "true".to_string()));
147    }
148
149    headers
150}
151
152/// The `X-Initiator` this request declares: `agent` once the turn contains
153/// anything the model itself produced, `user` otherwise.
154pub(crate) fn request_initiator(request: &completion::CompletionRequest) -> &'static str {
155    use crate::completion::Message;
156    let agent = request.chat_history.iter().any(|message| match message {
157        Message::Assistant(_) => true,
158        Message::User { content } => content
159            .iter()
160            .any(|item| matches!(item, crate::message::UserContent::ToolResult(_))),
161        Message::System { .. } => false,
162    });
163    if agent { "agent" } else { "user" }
164}
165
166/// Whether this request carries an image, which Copilot gates behind
167/// `copilot-vision-request`.
168pub(crate) fn request_has_vision(request: &completion::CompletionRequest) -> bool {
169    request.chat_history.iter().any(|message| match message {
170        crate::completion::Message::User { content } => content
171            .iter()
172            .any(|item| matches!(item, crate::message::UserContent::Image(_))),
173        _ => false,
174    })
175}
176
177#[cfg(test)]
178mod tests;