codex_convert_proxy/
cli.rs1use crate::config::{BackendConfig, MatchRules, ProxyConfig};
6use clap::{Args, Parser, Subcommand};
7use std::path::PathBuf;
8
9#[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#[derive(Subcommand, Debug)]
21pub enum Commands {
22 Start(StartArgs),
24 Init(InitArgs),
26}
27
28#[derive(Args, Debug)]
30pub struct StartArgs {
31 #[arg(short, long)]
33 pub config: Option<PathBuf>,
34
35 #[arg(short, long)]
37 pub provider: Option<String>,
38
39 #[arg(short, long)]
41 pub upstream_url: Option<String>,
42
43 #[arg(short, long)]
45 pub api_key: Option<String>,
46
47 #[arg(short, long)]
49 pub listen: Option<String>,
50
51 #[arg(long, default_value = "./logs")]
53 pub log_dir: PathBuf,
54
55 #[arg(long, default_value = "false")]
57 pub log_body: bool,
58}
59
60#[derive(Args, Debug)]
62pub struct InitArgs {
63 #[arg(default_value = "config.json")]
65 pub output: PathBuf,
66}
67
68impl Cli {
69 pub fn parse_args() -> Self {
71 Self::parse()
72 }
73}
74
75impl StartArgs {
76 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 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#[derive(Debug, Clone)]
122pub struct ResolvedConfig {
123 pub proxy: ProxyConfig,
124 pub backend: BackendConfig,
125 pub provider_name: String,
126}
127
128impl ResolvedConfig {
129 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 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}