Skip to main content

codex_convert_proxy/
cli.rs

1//! CLI argument parsing module.
2//!
3//! This module provides command-line argument parsing using clap.
4
5use crate::config::{BackendConfig, MatchRules, ProxyConfig};
6use clap::{Args, Parser, Subcommand};
7use std::path::PathBuf;
8
9/// Codex Convert Proxy CLI.
10#[derive(Parser, Debug)]
11#[command(name = "codex-convert-proxy")]
12#[command(version = env!("CARGO_PKG_VERSION"))]
13#[command(about = "Proxy for converting between OpenAI Responses API and Chat API")]
14pub struct Cli {
15    #[command(subcommand)]
16    pub command: Commands,
17}
18
19/// CLI commands.
20#[derive(Subcommand, Debug)]
21pub enum Commands {
22    /// Start the proxy server.
23    Start(StartArgs),
24    /// Generate a config file template.
25    Init(InitArgs),
26}
27
28/// Start the proxy server.
29#[derive(Args, Debug)]
30pub struct StartArgs {
31    /// Config file path.
32    #[arg(short, long)]
33    pub config: Option<PathBuf>,
34
35    /// Provider name (glm, kimi, deepseek, minimax).
36    #[arg(short, long)]
37    pub provider: Option<String>,
38
39    /// Upstream URL.
40    #[arg(short, long)]
41    pub upstream_url: Option<String>,
42
43    /// API key for the upstream provider.
44    #[arg(short, long)]
45    pub api_key: Option<String>,
46
47    /// Listen address.
48    #[arg(short, long)]
49    pub listen: Option<String>,
50
51    /// Log directory.
52    #[arg(long, default_value = "./logs")]
53    pub log_dir: PathBuf,
54
55    /// Log request/response bodies.
56    #[arg(long, default_value = "false")]
57    pub log_body: bool,
58}
59
60/// Initialize/generate config file.
61#[derive(Args, Debug)]
62pub struct InitArgs {
63    /// Output path for config file.
64    #[arg(default_value = "config.json")]
65    pub output: PathBuf,
66}
67
68impl Cli {
69    /// Parse command line arguments.
70    pub fn parse_args() -> Self {
71        Self::parse()
72    }
73}
74
75impl StartArgs {
76    /// Build ProxyConfig from arguments.
77    pub fn to_proxy_config(&self) -> anyhow::Result<ProxyConfig> {
78        let mut config = if let Some(config_path) = &self.config {
79            if config_path.exists() {
80                let content = std::fs::read_to_string(config_path)?;
81                serde_json::from_str(&content)?
82            } else {
83                ProxyConfig::default()
84            }
85        } else {
86            ProxyConfig::default()
87        };
88
89        // Override with CLI args
90        if let Some(provider) = &self.provider {
91            let default_upstream = "https://api.example.com".to_string();
92            let default_api_key = String::new();
93
94            let upstream_url = self.upstream_url.as_ref().unwrap_or(&default_upstream);
95            let api_key = self.api_key.as_ref().unwrap_or(&default_api_key);
96
97            config.backends = vec![BackendConfig {
98                name: provider.clone(),
99                url: upstream_url.clone(),
100                api_key: api_key.clone(),
101                protocol: "openai".to_string(),
102                model: None,
103                match_rules: MatchRules {
104                    default: true,
105                    ..Default::default()
106                },
107            }];
108        }
109
110        if let Some(listen) = &self.listen {
111            config.listen = listen.clone();
112        }
113        config.log_dir = self.log_dir.to_string_lossy().to_string();
114        config.log_body = self.log_body;
115
116        Ok(config)
117    }
118}
119
120/// Resolved configuration that combines file and CLI args.
121#[derive(Debug, Clone)]
122pub struct ResolvedConfig {
123    pub proxy: ProxyConfig,
124    pub backend: BackendConfig,
125    pub provider_name: String,
126}
127
128impl ResolvedConfig {
129    /// Create from CLI args.
130    pub fn from_args(args: &StartArgs) -> anyhow::Result<Self> {
131        let proxy = args.to_proxy_config()?;
132
133        if proxy.backends.is_empty() {
134            return Err(anyhow::anyhow!("No backend configured"));
135        }
136
137        let backend = proxy.backends[0].clone();
138        let provider_name = backend.name.clone();
139
140        Ok(Self {
141            proxy,
142            backend,
143            provider_name,
144        })
145    }
146
147    /// Get listen address and port.
148    pub fn listen_addr(&self) -> (String, u16) {
149        let parts: Vec<&str> = self.proxy.listen.split(':').collect();
150        if parts.len() == 2 {
151            let port: u16 = parts[1].parse().unwrap_or(8080);
152            (parts[0].to_string(), port)
153        } else {
154            ("0.0.0.0".to_string(), 8080)
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_listen_addr_parsing() {
165        let config = ResolvedConfig {
166            proxy: ProxyConfig {
167                listen: "0.0.0.0:8080".to_string(),
168                ..Default::default()
169            },
170            backend: BackendConfig {
171                name: "test".to_string(),
172                url: "https://api.example.com".to_string(),
173                api_key: "xxx".to_string(),
174                protocol: "openai".to_string(),
175                model: None,
176                match_rules: MatchRules::default(),
177            },
178            provider_name: "test".to_string(),
179        };
180
181        let (host, port) = config.listen_addr();
182        assert_eq!(host, "0.0.0.0");
183        assert_eq!(port, 8080);
184    }
185}