magi-code 0.61.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    agent::cancellation::AgentCancellation,
    model_catalog::ModelCatalogEntry,
    providers::{
        HttpRequest, HttpTransport, ProviderEvent, ProviderRequest,
        openai_stream::stream_with_transport_parser, stream::StreamParser,
    },
};
use serde_json::Value;
use std::fmt;

use super::{
    Provider,
    bodies::{
        openai_compatible_chat_completions_body_with_protocol,
        openai_compatible_responses_body_with_support,
    },
    catalog::{
        fetch_model_catalog_response_text_cancellable,
        parse_openai_compatible_model_catalog_response,
    },
    headers::{model_catalog_headers, sse_json_headers},
};
use crate::config::CustomReasoningProtocol;

#[derive(Clone)]
pub struct OpenAiCompatibleProvider<T> {
    model: String,
    api_key: Option<String>,
    chat_completions_url: String,
    responses_url: String,
    models_url: String,
    use_responses_endpoint: bool,
    provider_id: String,
    transport: T,
    reasoning_protocol: CustomReasoningProtocol,
    max_output_tokens: Option<u64>,
    supports_text_verbosity: bool,
}

impl<T: fmt::Debug> fmt::Debug for OpenAiCompatibleProvider<T> {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter
            .debug_struct("OpenAiCompatibleProvider")
            .field("model", &self.model)
            .field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
            .field("chat_completions_url", &self.chat_completions_url)
            .field("responses_url", &self.responses_url)
            .field("models_url", &self.models_url)
            .field("use_responses_endpoint", &self.use_responses_endpoint)
            .field("provider_id", &self.provider_id)
            .field("transport", &self.transport)
            .finish()
    }
}

impl<T> OpenAiCompatibleProvider<T> {
    pub fn custom(
        provider_id: impl Into<String>,
        model: impl Into<String>,
        api_key: Option<String>,
        base_url: impl Into<String>,
        use_responses_endpoint: bool,
        transport: T,
    ) -> Self {
        let base_url = base_url.into();
        Self {
            model: model.into(),
            api_key,
            chat_completions_url: format!("{base_url}/chat/completions"),
            responses_url: format!("{base_url}/responses"),
            models_url: format!("{base_url}/models"),
            use_responses_endpoint,
            provider_id: provider_id.into(),
            reasoning_protocol: CustomReasoningProtocol::GptLike,
            max_output_tokens: None,
            supports_text_verbosity: false,
            transport,
        }
    }

    pub fn with_text_verbosity_support(mut self, supported: bool) -> Self {
        self.supports_text_verbosity = supported;
        self
    }
    pub fn with_reasoning_protocol(
        mut self,
        protocol: CustomReasoningProtocol,
        max_output_tokens: Option<u64>,
    ) -> Self {
        self.reasoning_protocol = protocol;
        self.max_output_tokens = max_output_tokens;
        self
    }

    pub fn build_http_request(&self, request: &ProviderRequest) -> HttpRequest {
        let (url, body) = if self.use_responses_endpoint {
            (
                self.responses_url.clone(),
                openai_compatible_responses_body_with_support(
                    self.supports_text_verbosity,
                    &self.model,
                    request,
                    self.reasoning_protocol,
                    self.max_output_tokens,
                ),
            )
        } else {
            (
                self.chat_completions_url.clone(),
                openai_compatible_chat_completions_body_with_protocol(
                    &self.model,
                    request,
                    self.reasoning_protocol,
                    self.max_output_tokens,
                ),
            )
        };
        HttpRequest {
            method: "POST".to_string(),
            url,
            headers: sse_json_headers(self.api_key.as_deref()),
            body,
        }
    }

    pub fn build_model_catalog_request(&self) -> HttpRequest {
        HttpRequest {
            method: "GET".to_string(),
            url: self.models_url.clone(),
            headers: model_catalog_headers(self.api_key.as_deref()),
            body: Value::Null,
        }
    }

    pub fn discover_model_catalog(&self) -> anyhow::Result<Vec<ModelCatalogEntry>> {
        self.discover_model_catalog_cancellable(&AgentCancellation::default())
    }

    pub fn discover_model_catalog_cancellable(
        &self,
        cancellation: &AgentCancellation,
    ) -> anyhow::Result<Vec<ModelCatalogEntry>> {
        let text = fetch_model_catalog_response_text_cancellable(
            self.build_model_catalog_request(),
            &self.provider_id,
            cancellation,
        )?;
        parse_openai_compatible_model_catalog_response(&self.provider_id, &text)
    }
}

impl<T: HttpTransport + Send + Sync> Provider for OpenAiCompatibleProvider<T> {
    fn stream_cancellable(
        &self,
        request: ProviderRequest,
        cancellation: &AgentCancellation,
        on_event: &mut dyn FnMut(ProviderEvent) -> anyhow::Result<()>,
    ) -> anyhow::Result<()> {
        let semantic_progress_timeout = request.semantic_progress_timeout_or_default();
        stream_with_transport_parser(
            &self.transport,
            self.build_http_request(&request),
            cancellation,
            semantic_progress_timeout,
            StreamParser::for_provider_model(&self.provider_id, &self.model),
            on_event,
        )
    }
}