ktxd 0.1.1

A local Responses API compatibility proxy for Codex CLI and Chat Completions backends.
Documentation
use crate::error::{ProxyError, Result};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, env, fs, net::SocketAddr, path::Path};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
    #[serde(default)]
    pub server: ServerConfig,
    #[serde(default)]
    pub models: BTreeMap<String, ModelConfig>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    #[serde(default = "default_bind")]
    pub bind: SocketAddr,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
    pub public_model: String,
    #[serde(default = "default_upstream_family")]
    pub upstream_family: String,
    pub upstream_deployment: String,
    pub upstream_model: String,
    pub chat_completions_url: String,
    pub auth_header: AuthHeaderKind,
    pub auth_env_var: String,
    #[serde(default)]
    pub send_model_in_body: bool,
    #[serde(default = "default_true")]
    pub include_stream_usage: bool,
    #[serde(default = "default_true")]
    pub retry_without_stream_options_on_4xx: bool,
    #[serde(default)]
    pub instruction_role: InstructionRole,
    #[serde(default = "default_display_name")]
    pub display_name: String,
    #[serde(default = "default_description")]
    pub description: String,
    #[serde(default = "default_context_window")]
    pub context_window: i64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum AuthHeaderKind {
    #[serde(rename = "api-key", alias = "api_key")]
    ApiKey,
    #[serde(rename = "authorization_bearer", alias = "authorization-bearer")]
    AuthorizationBearer,
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum InstructionRole {
    #[default]
    System,
    Developer,
}

impl Default for AppConfig {
    fn default() -> Self {
        let model = ModelConfig::default_deepseek();
        let mut models = BTreeMap::new();
        models.insert(model.public_model.clone(), model);
        Self {
            server: ServerConfig::default(),
            models,
        }
    }
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            bind: default_bind(),
        }
    }
}

impl ModelConfig {
    pub fn default_deepseek() -> Self {
        Self {
            public_model: "DeepSeek-V4-Pro".to_string(),
            upstream_family: default_upstream_family(),
            upstream_deployment: "DeepSeek-V4-Pro".to_string(),
            upstream_model: "DeepSeek-V4-Pro".to_string(),
            chat_completions_url: "http://127.0.0.1:9/openai/deployments/DeepSeek-V4-Pro/chat/completions?api-version=2024-05-01-preview".to_string(),
            auth_header: AuthHeaderKind::ApiKey,
            auth_env_var: "AZURE_AI_FOUNDRY_API_KEY".to_string(),
            send_model_in_body: false,
            include_stream_usage: true,
            retry_without_stream_options_on_4xx: true,
            instruction_role: InstructionRole::System,
            display_name: default_display_name(),
            description: default_description(),
            context_window: default_context_window(),
        }
    }

    pub fn auth_value(&self) -> Result<String> {
        env::var(&self.auth_env_var).map_err(|_| {
            ProxyError::Config(format!(
                "missing secret environment variable {}",
                self.auth_env_var
            ))
        })
    }
}

impl AppConfig {
    pub fn load(path: Option<&Path>) -> Result<Self> {
        match path {
            Some(config_path) => {
                let contents = fs::read_to_string(config_path)
                    .map_err(|error| ProxyError::Config(error.to_string()))?;
                toml::from_str(&contents).map_err(|error| ProxyError::Config(error.to_string()))
            }
            None => Ok(Self::default()),
        }
    }

    pub fn model(&self, slug: &str) -> Result<&ModelConfig> {
        self.models
            .get(slug)
            .or_else(|| {
                self.models
                    .values()
                    .find(|model| model.public_model == slug)
            })
            .ok_or_else(|| ProxyError::UnknownModel(slug.to_string()))
    }
}

fn default_true() -> bool {
    true
}

fn default_bind() -> SocketAddr {
    "127.0.0.1:3000"
        .parse()
        .expect("valid default bind address")
}

fn default_upstream_family() -> String {
    "chat_completions".to_string()
}

fn default_display_name() -> String {
    "DeepSeek V4 Pro".to_string()
}

fn default_description() -> String {
    "DeepSeek-V4-Pro via Azure AI Foundry Chat Completions".to_string()
}

fn default_context_window() -> i64 {
    1_000_000
}