coproxy 0.6.1

OpenAI-compatible API proxy backed by GitHub Copilot
Documentation
use anyhow::Context;
use reqwest::Client;
use serde::Deserialize;
use std::time::Duration;

const GITHUB_CLIENT_ID: &str = "Iv1.b507a08c87ecfe98";
const GITHUB_DEVICE_CODE_URL: &str = "https://github.com/login/device/code";
const GITHUB_ACCESS_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";

#[derive(Debug, Deserialize)]
struct DeviceCodeResponse {
    device_code: String,
    user_code: String,
    verification_uri: String,
    #[serde(default = "default_interval")]
    interval: u64,
    #[serde(default = "default_expires_in")]
    expires_in: u64,
}

fn default_interval() -> u64 {
    5
}

fn default_expires_in() -> u64 {
    900
}

#[derive(Debug, Deserialize)]
struct AccessTokenResponse {
    access_token: Option<String>,
    error: Option<String>,
}

pub async fn login_with_device_flow(client: &Client) -> anyhow::Result<String> {
    let response: DeviceCodeResponse = client
        .post(GITHUB_DEVICE_CODE_URL)
        .header("Accept", "application/json")
        .json(&serde_json::json!({
            "client_id": GITHUB_CLIENT_ID,
            "scope": "read:user"
        }))
        .send()
        .await
        .context("failed requesting GitHub device code")?
        .error_for_status()
        .context("GitHub rejected device code request")?
        .json()
        .await
        .context("failed parsing GitHub device code response")?;

    eprintln!(
        "\nGitHub authentication is required.\nVisit: {}\nCode: {}\nWaiting for authorization...\n",
        response.verification_uri, response.user_code
    );

    let mut poll_interval = Duration::from_secs(response.interval.max(5));
    let expires_at = tokio::time::Instant::now() + Duration::from_secs(response.expires_in.max(1));

    while tokio::time::Instant::now() < expires_at {
        tokio::time::sleep(poll_interval).await;

        let token_response: AccessTokenResponse = client
            .post(GITHUB_ACCESS_TOKEN_URL)
            .header("Accept", "application/json")
            .json(&serde_json::json!({
                "client_id": GITHUB_CLIENT_ID,
                "device_code": response.device_code,
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code"
            }))
            .send()
            .await
            .context("failed polling GitHub access token")?
            .json()
            .await
            .context("failed parsing GitHub access token response")?;

        if let Some(token) = token_response.access_token {
            eprintln!("Authentication succeeded.\n");
            return Ok(token);
        }

        match token_response.error.as_deref() {
            Some("authorization_pending") | None => {}
            Some("slow_down") => {
                poll_interval += Duration::from_secs(5);
            }
            Some("expired_token") => anyhow::bail!("GitHub device authorization expired"),
            Some(other) => anyhow::bail!("GitHub device login failed: {other}"),
        }
    }

    anyhow::bail!("Timed out waiting for GitHub device authorization")
}