nexil 0.6.2

Provider-agnostic LLM toolkit — streaming, tool calls, tape storage, OAuth
Documentation
use serde_json::Value;

use crate::adapter::ProviderAdapter;
use crate::clients::parsing::TransportKind;
use crate::core::errors::{ConduitError, ErrorKind};
use crate::core::execution::LLMCore;
use crate::core::request_builder::TransportCallRequest;

pub static OPENAI_ADAPTER: OpenAIAdapter = OpenAIAdapter;

pub struct OpenAIAdapter;

impl ProviderAdapter for OpenAIAdapter {
    fn build_request_url(&self, api_base: &str, transport: TransportKind) -> String {
        let base = api_base.trim_end_matches('/');
        match transport {
            TransportKind::Completion => format!("{}/chat/completions", base),
            TransportKind::Responses => format!("{}/responses", base),
            TransportKind::Messages => format!("{}/messages", base),
        }
    }

    fn build_request_body(
        &self,
        request: &TransportCallRequest,
        transport: TransportKind,
    ) -> Result<Value, ConduitError> {
        match transport {
            TransportKind::Completion => Ok(self.build_completion_body(request)),
            TransportKind::Responses => Ok(self.build_responses_body(request)),
            TransportKind::Messages => Err(ConduitError::new(
                ErrorKind::Config,
                "openai adapter does not support messages transport",
            )),
        }
    }
}

impl OpenAIAdapter {
    fn build_completion_body(&self, request: &TransportCallRequest) -> Value {
        let mut kwargs = LLMCore::decide_kwargs_for_provider(
            &request.provider_name,
            request.max_tokens,
            &request.kwargs,
        );
        kwargs = LLMCore::with_default_completion_stream_options(
            &request.provider_name,
            request.stream,
            &kwargs,
        );

        let mut body = serde_json::Map::new();
        body.insert("model".to_owned(), Value::String(request.model_id.clone()));
        body.insert(
            "messages".to_owned(),
            Value::Array(request.messages_payload.clone()),
        );
        if request.stream {
            body.insert("stream".to_owned(), Value::Bool(true));
        }
        if let Some(ref tools) = request.tools_payload
            && !tools.is_empty()
        {
            body.insert("tools".to_owned(), Value::Array(tools.clone()));
        }
        if let Some(ref effort) = request.reasoning_effort
            && !effort.is_null()
        {
            body.insert("reasoning_effort".to_owned(), effort.clone());
        }
        for (key, value) in kwargs {
            body.entry(key).or_insert(value);
        }
        Value::Object(body)
    }

    fn build_responses_body(&self, request: &TransportCallRequest) -> Value {
        let (instructions, input_items) =
            LLMCore::split_messages_for_responses(&request.messages_payload);

        let responses_kwargs =
            LLMCore::with_responses_reasoning(&request.kwargs, request.reasoning_effort.as_ref());
        let final_kwargs =
            LLMCore::decide_responses_kwargs(request.max_tokens, &responses_kwargs, true);

        let mut body = serde_json::Map::new();
        body.insert("model".to_owned(), Value::String(request.model_id.clone()));
        body.insert("input".to_owned(), Value::Array(input_items));
        body.insert(
            "instructions".to_owned(),
            Value::String(instructions.unwrap_or_default()),
        );
        if request.stream {
            body.insert("stream".to_owned(), Value::Bool(true));
        }
        if let Some(ref tools) = request.tools_payload
            && let Some(converted) = LLMCore::convert_tools_for_responses(Some(tools))
        {
            body.insert("tools".to_owned(), Value::Array(converted));
        }
        for (key, value) in final_kwargs {
            body.entry(key).or_insert(value);
        }

        if let Some(ref base) = request.api_base
            && base.contains("chatgpt.com")
        {
            body.insert("store".to_owned(), Value::Bool(false));
            body.insert("stream".to_owned(), Value::Bool(true));
            body.remove("max_output_tokens");
        }

        Value::Object(body)
    }
}