aiclient-api 0.1.0

A unified AI gateway daemon exposing OpenAI-compatible and Anthropic-compatible API endpoints, backed by GitHub Copilot and Kiro (AWS CodeWhisperer)
Documentation
use anyhow::{bail, Context, Result};
use serde::Deserialize;

use crate::providers::Model;
use super::client::CopilotClient;
use super::headers::CopilotHeaders;

#[derive(Debug, Deserialize)]
struct ModelCapabilities {
    #[serde(default)]
    supports_streaming: Option<bool>,
    #[serde(rename = "type")]
    #[allow(dead_code)]
    model_type: Option<String>,
    #[serde(default)]
    #[allow(dead_code)]
    tokenizer: Option<String>,
    limits: Option<ModelLimits>,
    supports: Option<ModelSupports>,
}

#[derive(Debug, Deserialize)]
struct ModelLimits {
    max_context_window_tokens: Option<u32>,
    max_output_tokens: Option<u32>,
    max_prompt_tokens: Option<u32>,
}

#[derive(Debug, Deserialize)]
struct ModelSupports {
    streaming: Option<bool>,
    tool_calls: Option<bool>,
    #[allow(dead_code)]
    parallel_tool_calls: Option<bool>,
}

#[derive(Debug, Deserialize)]
struct RawModel {
    id: String,
    name: Option<String>,
    vendor: Option<String>,
    capabilities: Option<ModelCapabilities>,
}

#[derive(Debug, Deserialize)]
struct ModelsResponse {
    data: Vec<RawModel>,
}

pub async fn fetch_models(
    client: &CopilotClient,
    headers: &CopilotHeaders,
    copilot_token: &str,
) -> Result<Vec<Model>> {
    let hdrs = headers.build(copilot_token);

    let resp = client
        .http_client()
        .get(format!("{}/models", client.base_url()))
        .headers(hdrs)
        .send()
        .await
        .context("Failed to fetch models from Copilot")?;

    if !resp.status().is_success() {
        let status = resp.status();
        let body = resp.text().await.unwrap_or_default();
        bail!("Failed to fetch models: HTTP {} - {}", status, body);
    }

    let models_resp: ModelsResponse = resp
        .json()
        .await
        .context("Failed to parse models response")?;

    let models = models_resp
        .data
        .into_iter()
        .map(|m| {
            let caps = m.capabilities.as_ref();
            let limits = caps.and_then(|c| c.limits.as_ref());
            let supports = caps.and_then(|c| c.supports.as_ref());

            let max_input_tokens = limits
                .and_then(|l| l.max_prompt_tokens.or(l.max_context_window_tokens));
            let max_output_tokens = limits.and_then(|l| l.max_output_tokens);

            let supports_streaming = supports
                .and_then(|s| s.streaming)
                .or_else(|| caps.and_then(|c| c.supports_streaming))
                .unwrap_or(true);

            let supports_tools = supports
                .and_then(|s| s.tool_calls)
                .unwrap_or(false);

            Model {
                id: format!("copilot/{}", m.id),
                provider: "copilot".to_string(),
                vendor: m.vendor.unwrap_or_else(|| "github".to_string()),
                display_name: m.name.unwrap_or_else(|| m.id.clone()),
                max_input_tokens,
                max_output_tokens,
                supports_streaming,
                supports_tools,
                supports_vision: false,
                supports_thinking: false,
            }
        })
        .collect();

    Ok(models)
}