ai 0.4.0

Simple to use LLM library for Rust with streaming, tool calling, OAuth helpers, and a lightweight agent loop
Documentation
use std::sync::Arc;

use crate::env_api_keys::{KnownProvider, get_env_api_key};
use crate::event_stream::AssistantEventStream;
use crate::oauth::{GitHubCopilotOAuthProvider, OAuthApiKey, OAuthCredentials};
use crate::provider::{LanguageModelApi, ModelBuilder, Provider, ProviderCapabilities};
use crate::providers::{anthropic, openai_completions, openai_responses, register_builtins};
use crate::types::{Context, Model, ModelInput, SimpleStreamOptions, StreamOptions};
use crate::{Error, Result};

const DEFAULT_PROVIDER_ID: KnownProvider = KnownProvider::GitHubCopilot;
const DEFAULT_BASE_URL: &str = "https://api.individual.githubcopilot.com";

#[derive(Clone)]
pub struct GitHubCopilot {
    provider_id: String,
    api_key: Option<String>,
    base_url: Option<String>,
    api: Option<GitHubCopilotApi>,
    http_client: Option<reqwest::Client>,
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum GitHubCopilotApi {
    AnthropicMessages,
    OpenAiChatCompletions,
    #[default]
    OpenAiResponses,
}

impl GitHubCopilotApi {
    pub const fn id(self) -> &'static str {
        match self {
            Self::AnthropicMessages => "anthropic-messages",
            Self::OpenAiChatCompletions => "openai-completions",
            Self::OpenAiResponses => "openai-responses",
        }
    }
}

impl GitHubCopilot {
    pub fn builder() -> GitHubCopilotBuilder {
        GitHubCopilotBuilder::default()
    }

    pub fn from_env() -> Result<Self> {
        let api_key = get_env_api_key(DEFAULT_PROVIDER_ID)
            .filter(|key| !key.trim().is_empty())
            .ok_or_else(|| Error::MissingApiKey(DEFAULT_PROVIDER_ID.into()))?;
        Self::builder().api_key(api_key).build()
    }

    pub fn model(&self, id: &str) -> ModelBuilder {
        <Self as Provider>::model(self, id)
    }
}

impl Provider for GitHubCopilot {
    fn id(&self) -> &str {
        &self.provider_id
    }

    fn capabilities(&self) -> ProviderCapabilities {
        ProviderCapabilities {
            language_models: true,
            image_models: false,
        }
    }

    fn model(&self, id: &str) -> ModelBuilder {
        let api = self.api.unwrap_or_default();
        let runtime = Arc::new(GitHubCopilotLanguageModelApi {
            api,
            api_key: self.api_key.clone(),
            http_client: self.http_client.clone(),
        });
        let base_url = self
            .base_url
            .clone()
            .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
        ModelBuilder::new(&self.provider_id, id, runtime)
            .base_url(base_url)
            .input(vec![ModelInput::Text, ModelInput::Image])
            .context_window(1_000_000)
            .max_tokens(16_384)
    }
}

#[derive(Default)]
pub struct GitHubCopilotBuilder {
    provider_id: Option<String>,
    api_key: Option<String>,
    base_url: Option<String>,
    api: Option<GitHubCopilotApi>,
    http_client: Option<reqwest::Client>,
}

impl GitHubCopilotBuilder {
    pub fn provider_id(mut self, provider_id: impl Into<String>) -> Self {
        self.provider_id = Some(provider_id.into());
        self
    }

    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
        self.api_key = Some(api_key.into());
        self
    }

    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
        self.base_url = Some(base_url.into());
        self
    }

    pub fn api(mut self, api: GitHubCopilotApi) -> Self {
        self.api = Some(api);
        self
    }

    pub fn anthropic_messages(mut self) -> Self {
        self.api = Some(GitHubCopilotApi::AnthropicMessages);
        self
    }

    pub fn chat_completions(mut self) -> Self {
        self.api = Some(GitHubCopilotApi::OpenAiChatCompletions);
        self
    }

    pub fn responses(mut self) -> Self {
        self.api = Some(GitHubCopilotApi::OpenAiResponses);
        self
    }

    pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
        self.http_client = Some(http_client);
        self
    }

    pub fn build(self) -> Result<GitHubCopilot> {
        Ok(GitHubCopilot {
            provider_id: self
                .provider_id
                .unwrap_or_else(|| DEFAULT_PROVIDER_ID.into()),
            api_key: self.api_key,
            base_url: self.base_url,
            api: self.api,
            http_client: self.http_client,
        })
    }
}

#[derive(Clone)]
struct GitHubCopilotLanguageModelApi {
    api: GitHubCopilotApi,
    api_key: Option<String>,
    http_client: Option<reqwest::Client>,
}

impl GitHubCopilotLanguageModelApi {
    fn with_api_key(&self, mut options: StreamOptions) -> StreamOptions {
        if options
            .api_key
            .as_deref()
            .is_none_or(|api_key| api_key.trim().is_empty())
        {
            options.api_key = self.api_key.clone();
        }
        if options.http_client.is_none() {
            options.http_client = self.http_client.clone();
        }
        options
    }

    fn with_api_key_simple(&self, mut options: SimpleStreamOptions) -> SimpleStreamOptions {
        options.stream = self.with_api_key(options.stream);
        options
    }
}

impl LanguageModelApi for GitHubCopilotLanguageModelApi {
    fn id(&self) -> &str {
        self.api.id()
    }

    fn stream(
        &self,
        model: Model,
        context: Context,
        options: StreamOptions,
    ) -> Result<AssistantEventStream> {
        let options = self.with_api_key(options);
        match self.api {
            GitHubCopilotApi::AnthropicMessages => Ok(anthropic::stream_anthropic(
                model,
                context,
                register_builtins::anthropic_options_from_stream_options(options),
            )),
            GitHubCopilotApi::OpenAiChatCompletions => {
                Ok(openai_completions::stream_openai_completions(
                    model,
                    context,
                    register_builtins::openai_completions_options_from_stream_options(options),
                ))
            }
            GitHubCopilotApi::OpenAiResponses => Ok(openai_responses::stream_openai_responses(
                model,
                context,
                register_builtins::openai_responses_options_from_stream_options(options),
            )),
        }
    }

    fn stream_simple(
        &self,
        model: Model,
        context: Context,
        options: SimpleStreamOptions,
    ) -> Result<AssistantEventStream> {
        let options = self.with_api_key_simple(options);
        match self.api {
            GitHubCopilotApi::AnthropicMessages => {
                anthropic::stream_simple_anthropic(model, context, options)
            }
            GitHubCopilotApi::OpenAiChatCompletions => {
                openai_completions::stream_simple_openai_completions(model, context, options)
            }
            GitHubCopilotApi::OpenAiResponses => {
                openai_responses::stream_simple_openai_responses(model, context, options)
            }
        }
    }
}

pub fn builder() -> GitHubCopilotBuilder {
    GitHubCopilot::builder()
}

pub fn from_env() -> Result<GitHubCopilot> {
    GitHubCopilot::from_env()
}

pub fn oauth() -> GitHubCopilotOAuthProvider {
    crate::oauth::github_copilot_oauth_provider()
}

pub fn base_url(token: Option<&str>, enterprise_domain: Option<&str>) -> String {
    crate::oauth::get_github_copilot_base_url(token, enterprise_domain)
}

pub fn base_url_for_credentials(credentials: &OAuthCredentials) -> String {
    base_url(
        Some(&credentials.access),
        crate::oauth::github_copilot_enterprise_domain(credentials),
    )
}

pub async fn get_oauth_api_key(credentials: &OAuthCredentials) -> Result<OAuthApiKey> {
    let credentials = if crate::utils::time::now_millis() >= credentials.expires {
        oauth().refresh_token(credentials).await?
    } else {
        credentials.clone()
    };
    let api_key = oauth().get_api_key(&credentials);
    Ok(OAuthApiKey {
        new_credentials: credentials,
        api_key,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_model_uses_responses_api_without_catalog_metadata() {
        let provider = builder().api_key("test-token").build().expect("provider");
        let model = provider.model("claude-opus-4.5").build().expect("model");

        assert_eq!(model.provider_id(), "github-copilot");
        assert_eq!(model.api_id(), "openai-responses");
        assert_eq!(model.base_url, DEFAULT_BASE_URL);
        assert!(model.headers.is_empty());
    }

    #[test]
    fn explicit_api_supports_unknown_model_ids() {
        let provider = builder()
            .api_key("test-token")
            .chat_completions()
            .base_url("https://copilot.example")
            .build()
            .expect("provider");
        let model = provider.model("future-model").build().expect("model");

        assert_eq!(model.id(), "future-model");
        assert_eq!(model.api_id(), "openai-completions");
        assert_eq!(model.base_url, "https://copilot.example");
    }
}