rig-core 0.44.0

An opinionated library for building LLM powered applications.
Documentation
//! GitHub Copilot authentication, endpoint wires, and model identifiers.
//!
//! A [`Copilot`] client sends with an exchanged session token. To log in with
//! a device code or refresh a GitHub token, pass an [`auth::Authenticator`] to
//! [`Copilot::authenticate`], which resolves the session through the client's
//! own transport.
//! Completion routes are selected by model; all routes include editor-identity headers.
//!
//! ```no_run
//! use rig_core::providers::copilot;
//!
//! # fn run() -> Result<(), Box<dyn std::error::Error>> {
//! let github = copilot::Copilot::from_env()?;
//!
//! let chat = github.completion(copilot::GPT_4O);
//! let codex = github.completion(copilot::GPT_5_3_CODEX);
//! let embeddings = github.embedding(copilot::TEXT_EMBEDDING_3_SMALL, None);
//! # let _ = (chat, codex, embeddings);
//! # Ok(())
//! # }
//! ```

/// Device login, session-token exchange, refresh, and shared credential caching.
///
/// ```no_run
/// use rig_core::providers::copilot::auth::{AuthSource, Authenticator, DeviceCodeHandler};
///
/// let auth = Authenticator::new(AuthSource::OAuth, None, None, DeviceCodeHandler::default(), true);
/// ```
pub mod auth;
pub mod extension;
pub mod model_listing;
pub mod wire;

pub use crate::client::copilot::Copilot;
pub use wire::CopilotConfig;

use crate::completion;
use crate::providers::openai;
use serde::{Deserialize, Serialize};

/// The API root a token that names no endpoint of its own resolves against.
pub(crate) const GITHUB_COPILOT_API_BASE_URL: &str = "https://api.githubcopilot.com";
pub(crate) const EDITOR_PLUGIN_VERSION: &str = "copilot-chat/0.35.0";
pub(crate) const USER_AGENT: &str = "GitHubCopilotChat/0.35.0";
pub(crate) const EDITOR_VERSION: &str = "vscode/1.107.0";
const API_VERSION: &str = "2025-04-01";

/// Stable descriptor name reported on normalized Copilot responses.
pub const PROVIDER_NAME: &str = "copilot";

/// Conversation intent sent in the `openai-intent` request header.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CopilotIntent {
    #[default]
    Panel,
    Edits,
}

impl CopilotIntent {
    pub fn as_header(self) -> &'static str {
        match self {
            CopilotIntent::Panel => "conversation-panel",
            CopilotIntent::Edits => "conversation-edits",
        }
    }
}

/// `gpt-4`
pub const GPT_4: &str = "gpt-4";
/// `gpt-4o`
pub const GPT_4O: &str = "gpt-4o";
/// `gpt-4o-mini`
pub const GPT_4O_MINI: &str = "gpt-4o-mini";
/// `gpt-4.1`
pub const GPT_4_1: &str = "gpt-4.1";
/// `gpt-4.1-mini`
pub const GPT_4_1_MINI: &str = "gpt-4.1-mini";
/// `gpt-4.1-nano`
pub const GPT_4_1_NANO: &str = "gpt-4.1-nano";
/// `gpt-5.3-codex`
pub const GPT_5_3_CODEX: &str = "gpt-5.3-codex";
/// `gpt-5.1-codex`
pub const GPT_5_1_CODEX: &str = "gpt-5.1-codex";
/// `gpt-5.5`
pub const GPT_5_5: &str = "gpt-5.5";
/// `gpt-5.4`
pub const GPT_5_4: &str = "gpt-5.4";
/// `claude-sonnet-4` completion model (Anthropic, via Copilot)
pub const CLAUDE_SONNET_4: &str = "claude-sonnet-4";
/// `claude-sonnet-4.6`
pub const CLAUDE_SONNET_4_6: &str = "claude-sonnet-4.6";
/// `claude-opus-4.6`
pub const CLAUDE_OPUS_4_6: &str = "claude-opus-4.6";
/// `claude-opus-4.7`
pub const CLAUDE_OPUS_4_7: &str = "claude-opus-4.7";
/// `claude-3.5-sonnet` completion model (Anthropic, via Copilot)
pub const CLAUDE_3_5_SONNET: &str = "claude-3.5-sonnet";
/// `gemini-3-flash-preview` completion model (Google, via Copilot)
pub const GEMINI_3_FLASH: &str = "gemini-3-flash-preview";
/// `gemini-3.1-pro-preview` completion model (Google, via Copilot)
pub const GEMINI_3_1_PRO_FLASH: &str = "gemini-3.1-pro-preview";
/// `gemini-2.0-flash-001` completion model (Google, via Copilot)
pub const GEMINI_2_0_FLASH: &str = "gemini-2.0-flash-001";
/// `o3-mini` reasoning model (OpenAI, via Copilot)
pub const O3_MINI: &str = "o3-mini";
/// `text-embedding-3-small`
pub const TEXT_EMBEDDING_3_SMALL: &str = "text-embedding-3-small";
/// `text-embedding-3-large`
pub const TEXT_EMBEDDING_3_LARGE: &str = "text-embedding-3-large";
/// `text-embedding-ada-002`
pub const TEXT_EMBEDDING_ADA_002: &str = "text-embedding-ada-002";

/// The encoding [`wire::Embeddings::with_encoding_format`] asks for. Copilot
/// relays OpenAI's embeddings wire, so it is OpenAI's own type.
pub use openai::EncodingFormat;

/// Build required editor-identity headers, including credentials and request identity.
pub(crate) fn default_headers(
    api_key: &str,
    initiator: &'static str,
    has_vision: bool,
    intent: CopilotIntent,
) -> Vec<(&'static str, String)> {
    let mut headers = vec![
        (
            http::header::AUTHORIZATION.as_str(),
            format!("Bearer {api_key}"),
        ),
        ("copilot-integration-id", "vscode-chat".to_string()),
        ("editor-version", EDITOR_VERSION.to_string()),
        ("editor-plugin-version", EDITOR_PLUGIN_VERSION.to_string()),
        ("user-agent", USER_AGENT.to_string()),
        ("openai-intent", intent.as_header().to_string()),
        ("x-github-api-version", API_VERSION.to_string()),
        ("x-request-id", crate::id::generate()),
        (
            "x-vscode-user-agent-library-version",
            "electron-fetch".to_string(),
        ),
        ("X-Initiator", initiator.to_string()),
    ];

    if has_vision {
        headers.push(("copilot-vision-request", "true".to_string()));
    }

    headers
}

/// The `X-Initiator` this request declares: `agent` once the turn contains
/// anything the model itself produced, `user` otherwise.
pub(crate) fn request_initiator(request: &completion::CompletionRequest) -> &'static str {
    use crate::completion::Message;
    let agent = request.chat_history.iter().any(|message| match message {
        Message::Assistant(_) => true,
        Message::User { content } => content
            .iter()
            .any(|item| matches!(item, crate::message::UserContent::ToolResult(_))),
        Message::System { .. } => false,
    });
    if agent { "agent" } else { "user" }
}

/// Whether this request carries an image, which Copilot gates behind
/// `copilot-vision-request`.
pub(crate) fn request_has_vision(request: &completion::CompletionRequest) -> bool {
    request.chat_history.iter().any(|message| match message {
        crate::completion::Message::User { content } => content
            .iter()
            .any(|item| matches!(item, crate::message::UserContent::Image(_))),
        _ => false,
    })
}

#[cfg(test)]
mod tests;