1use clap::Parser;
2use serde::Serialize;
3use std::{net::SocketAddr, time::Duration};
4
5pub const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 300;
6pub const DEFAULT_STREAM_IDLE_TIMEOUT_SECS: u64 = 300;
7
8#[derive(Parser, Debug, Clone)]
9#[command(name = "maple-proxy")]
10#[command(about = "Lightweight OpenAI-compatible proxy server for Maple/OpenSecret")]
11pub struct Config {
12 #[arg(long, env = "MAPLE_HOST", default_value = "127.0.0.1")]
14 pub host: String,
15
16 #[arg(short, long, env = "MAPLE_PORT", default_value = "8080")]
18 pub port: u16,
19
20 #[arg(
22 long,
23 env = "MAPLE_BACKEND_URL",
24 default_value = "https://enclave.trymaple.ai"
25 )]
26 pub backend_url: String,
27
28 #[arg(long, env = "MAPLE_API_KEY")]
30 pub default_api_key: Option<String>,
31
32 #[arg(short, long, env = "MAPLE_DEBUG")]
34 pub debug: bool,
35
36 #[arg(long, env = "MAPLE_ENABLE_CORS")]
38 pub enable_cors: bool,
39
40 #[arg(
42 long,
43 env = "MAPLE_REQUEST_TIMEOUT_SECS",
44 default_value_t = DEFAULT_REQUEST_TIMEOUT_SECS,
45 value_parser = clap::value_parser!(u64).range(1..)
46 )]
47 pub request_timeout_secs: u64,
48
49 #[arg(
51 long,
52 env = "MAPLE_STREAM_IDLE_TIMEOUT_SECS",
53 default_value_t = DEFAULT_STREAM_IDLE_TIMEOUT_SECS,
54 value_parser = clap::value_parser!(u64).range(1..)
55 )]
56 pub stream_idle_timeout_secs: u64,
57}
58
59impl Config {
60 pub fn socket_addr(&self) -> anyhow::Result<SocketAddr> {
61 let addr = format!("{}:{}", self.host, self.port);
62 addr.parse()
63 .map_err(|e| anyhow::anyhow!("Invalid socket address '{}': {}", addr, e))
64 }
65
66 pub fn load() -> Self {
67 let _ = dotenvy::dotenv();
69
70 Config::parse()
71 }
72
73 pub fn new(host: String, port: u16, backend_url: String) -> Self {
75 Self {
76 host,
77 port,
78 backend_url,
79 default_api_key: None,
80 debug: false,
81 enable_cors: false,
82 request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
83 stream_idle_timeout_secs: DEFAULT_STREAM_IDLE_TIMEOUT_SECS,
84 }
85 }
86
87 pub fn request_timeout(&self) -> Duration {
88 Duration::from_secs(self.request_timeout_secs)
89 }
90
91 pub fn stream_idle_timeout(&self) -> Duration {
92 Duration::from_secs(self.stream_idle_timeout_secs)
93 }
94
95 pub fn with_api_key(mut self, api_key: String) -> Self {
97 self.default_api_key = Some(api_key);
98 self
99 }
100
101 pub fn with_debug(mut self, debug: bool) -> Self {
103 self.debug = debug;
104 self
105 }
106
107 pub fn with_cors(mut self, enable_cors: bool) -> Self {
109 self.enable_cors = enable_cors;
110 self
111 }
112
113 pub fn with_request_timeout_secs(mut self, request_timeout_secs: u64) -> Self {
115 self.request_timeout_secs = request_timeout_secs;
116 self
117 }
118
119 pub fn with_stream_idle_timeout_secs(mut self, stream_idle_timeout_secs: u64) -> Self {
121 self.stream_idle_timeout_secs = stream_idle_timeout_secs;
122 self
123 }
124}
125
126#[derive(Debug, Serialize)]
127pub(crate) struct OpenAIError {
128 error: OpenAIErrorDetails,
129}
130
131#[derive(Debug, Serialize)]
132struct OpenAIErrorDetails {
133 message: String,
134 #[serde(rename = "type")]
135 error_type: String,
136 param: Option<String>,
137 code: Option<String>,
138}
139
140impl OpenAIError {
141 fn new(message: impl Into<String>, error_type: impl Into<String>) -> Self {
142 Self {
143 error: OpenAIErrorDetails {
144 message: message.into(),
145 error_type: error_type.into(),
146 param: None,
147 code: None,
148 },
149 }
150 }
151
152 pub(crate) fn authentication_error(message: impl Into<String>) -> Self {
153 Self::new(message, "invalid_request_error")
154 }
155
156 pub(crate) fn server_error(message: impl Into<String>) -> Self {
157 Self::new(message, "server_error")
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use clap::{error::ErrorKind, Parser};
165
166 #[test]
167 fn config_new_uses_timeout_defaults() {
168 let config = Config::new(
169 "127.0.0.1".to_string(),
170 8080,
171 "https://enclave.trymaple.ai".to_string(),
172 );
173
174 assert_eq!(config.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS);
175 assert_eq!(
176 config.stream_idle_timeout_secs,
177 DEFAULT_STREAM_IDLE_TIMEOUT_SECS
178 );
179 assert_eq!(
180 config.request_timeout(),
181 Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS)
182 );
183 assert_eq!(
184 config.stream_idle_timeout(),
185 Duration::from_secs(DEFAULT_STREAM_IDLE_TIMEOUT_SECS)
186 );
187 }
188
189 #[test]
190 fn timeout_builder_methods_override_defaults() {
191 let config = Config::new(
192 "127.0.0.1".to_string(),
193 8080,
194 "https://enclave.trymaple.ai".to_string(),
195 )
196 .with_request_timeout_secs(45)
197 .with_stream_idle_timeout_secs(15);
198
199 assert_eq!(config.request_timeout(), Duration::from_secs(45));
200 assert_eq!(config.stream_idle_timeout(), Duration::from_secs(15));
201 }
202
203 #[test]
204 fn timeout_cli_values_must_be_positive() {
205 let request_timeout_error =
206 Config::try_parse_from(["maple-proxy", "--request-timeout-secs", "0"]).unwrap_err();
207 assert_eq!(request_timeout_error.kind(), ErrorKind::ValueValidation);
208
209 let stream_idle_timeout_error =
210 Config::try_parse_from(["maple-proxy", "--stream-idle-timeout-secs", "0"]).unwrap_err();
211 assert_eq!(stream_idle_timeout_error.kind(), ErrorKind::ValueValidation);
212 }
213}