Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,11 @@ MAPLE_DEBUG=false
# Recommended: true for Docker deployments
MAPLE_ENABLE_CORS=true

# Timeouts
# Backend request setup and non-streaming response timeout, in seconds
MAPLE_REQUEST_TIMEOUT_SECS=300
# Maximum idle time between streaming chunks, in seconds
MAPLE_STREAM_IDLE_TIMEOUT_SECS=300

# Rust Logging (optional)
# RUST_LOG=info,maple_proxy=debug
# RUST_LOG=info,maple_proxy=debug
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ Environment variables (can be set in .env file):
- `MAPLE_API_KEY` - Default API key (optional)
- `MAPLE_DEBUG` - Enable debug logging
- `MAPLE_ENABLE_CORS` - Enable CORS for web clients
- `MAPLE_REQUEST_TIMEOUT_SECS` - Backend request timeout in seconds (default: 300)
- `MAPLE_STREAM_IDLE_TIMEOUT_SECS` - Streaming idle timeout in seconds (default: 300)

## Testing

Expand All @@ -96,4 +98,4 @@ Key dependencies:
- **tokio** - Async runtime
- **tower/tower-http** - Middleware for CORS and tracing
- **clap** - CLI argument parsing
- **dotenvy** - .env file support
- **dotenvy** - .env file support
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ opensecret = "3.1.1"

# Web server
axum = { version = "0.8.4", features = ["http2", "macros"] }
tokio = { version = "1.47", features = ["net", "rt-multi-thread", "macros", "sync"] }
tokio = { version = "1.47", features = ["net", "rt-multi-thread", "macros", "sync", "time"] }
tower = { version = "0.5.2", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors", "trace"] }

Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ ENV MAPLE_HOST=0.0.0.0 \
MAPLE_BACKEND_URL=https://enclave.trymaple.ai \
MAPLE_DEBUG=false \
MAPLE_ENABLE_CORS=true \
MAPLE_REQUEST_TIMEOUT_SECS=300 \
MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 \
RUST_LOG=info

# Expose the port
Expand All @@ -67,4 +69,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1

# Run the binary
ENTRYPOINT ["/usr/local/bin/maple-proxy"]
ENTRYPOINT ["/usr/local/bin/maple-proxy"]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export MAPLE_BACKEND_URL=http://localhost:3000 # Maple backend URL (prod
export MAPLE_API_KEY=your-maple-api-key # Default API key (optional)
export MAPLE_DEBUG=true # Enable debug logging
export MAPLE_ENABLE_CORS=true # Enable CORS
export MAPLE_REQUEST_TIMEOUT_SECS=300 # Backend request timeout
export MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 # Streaming idle timeout between chunks
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

Or use CLI arguments:
Expand Down Expand Up @@ -242,6 +244,8 @@ docker pull ghcr.io/opensecretcloud/maple-proxy:latest
# Run with your API key
docker run -p 8080:8080 \
-e MAPLE_BACKEND_URL=https://enclave.trymaple.ai \
-e MAPLE_REQUEST_TIMEOUT_SECS=300 \
-e MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 \
ghcr.io/opensecretcloud/maple-proxy:latest
```

Expand Down Expand Up @@ -340,6 +344,8 @@ The Docker image:
environment:
- MAPLE_BACKEND_URL=https://enclave.trymaple.ai # Production backend
- MAPLE_ENABLE_CORS=true # Enable for web apps
- MAPLE_REQUEST_TIMEOUT_SECS=300 # Backend request timeout
- MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 # Streaming idle timeout
- RUST_LOG=info # Logging level
# - MAPLE_API_KEY=xxx # Only for private deployments!
```
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ services:
# Optional configurations
- MAPLE_DEBUG=${MAPLE_DEBUG:-false}
- MAPLE_ENABLE_CORS=${MAPLE_ENABLE_CORS:-true}
- MAPLE_REQUEST_TIMEOUT_SECS=${MAPLE_REQUEST_TIMEOUT_SECS:-300}
- MAPLE_STREAM_IDLE_TIMEOUT_SECS=${MAPLE_STREAM_IDLE_TIMEOUT_SECS:-300}
- RUST_LOG=${RUST_LOG:-info}

# Production-grade restart policy
Expand Down Expand Up @@ -62,4 +64,4 @@ services:

networks:
maple-network:
driver: bridge
driver: bridge
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ env:
@echo "MAPLE_API_KEY: ${MAPLE_API_KEY:-[not set]}"
@echo "MAPLE_DEBUG: ${MAPLE_DEBUG:-false}"
@echo "MAPLE_ENABLE_CORS: ${MAPLE_ENABLE_CORS:-false}"
@echo "MAPLE_REQUEST_TIMEOUT_SECS: ${MAPLE_REQUEST_TIMEOUT_SECS:-300}"
@echo "MAPLE_STREAM_IDLE_TIMEOUT_SECS: ${MAPLE_STREAM_IDLE_TIMEOUT_SECS:-300}"

# Build Docker image
docker-build:
Expand All @@ -182,6 +184,8 @@ docker-run:
-e MAPLE_BACKEND_URL=${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai} \
-e MAPLE_DEBUG=${MAPLE_DEBUG:-false} \
-e MAPLE_ENABLE_CORS=${MAPLE_ENABLE_CORS:-true} \
-e MAPLE_REQUEST_TIMEOUT_SECS=${MAPLE_REQUEST_TIMEOUT_SECS:-300} \
-e MAPLE_STREAM_IDLE_TIMEOUT_SECS=${MAPLE_STREAM_IDLE_TIMEOUT_SECS:-300} \
maple-proxy:latest

# Run Docker container in detached mode
Expand All @@ -194,6 +198,8 @@ docker-run-detached:
-e MAPLE_BACKEND_URL=${MAPLE_BACKEND_URL:-https://enclave.trymaple.ai} \
-e MAPLE_DEBUG=${MAPLE_DEBUG:-false} \
-e MAPLE_ENABLE_CORS=${MAPLE_ENABLE_CORS:-true} \
-e MAPLE_REQUEST_TIMEOUT_SECS=${MAPLE_REQUEST_TIMEOUT_SECS:-300} \
-e MAPLE_STREAM_IDLE_TIMEOUT_SECS=${MAPLE_STREAM_IDLE_TIMEOUT_SECS:-300} \
maple-proxy:latest
@echo "✅ Container started. Use 'just docker-stop' to stop it."

Expand Down
99 changes: 98 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::{net::SocketAddr, time::Duration};

pub const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 300;
pub const DEFAULT_STREAM_IDLE_TIMEOUT_SECS: u64 = 300;

#[derive(Parser, Debug, Clone)]
#[command(name = "maple-proxy")]
Expand Down Expand Up @@ -33,6 +36,24 @@ pub struct Config {
/// Enable CORS for all origins (useful for web clients)
#[arg(long, env = "MAPLE_ENABLE_CORS")]
pub enable_cors: bool,

/// Timeout for backend request setup and non-streaming responses, in seconds
#[arg(
long,
env = "MAPLE_REQUEST_TIMEOUT_SECS",
default_value_t = DEFAULT_REQUEST_TIMEOUT_SECS,
value_parser = clap::value_parser!(u64).range(1..)
)]
pub request_timeout_secs: u64,

/// Maximum time to wait between streaming response chunks, in seconds
#[arg(
long,
env = "MAPLE_STREAM_IDLE_TIMEOUT_SECS",
default_value_t = DEFAULT_STREAM_IDLE_TIMEOUT_SECS,
value_parser = clap::value_parser!(u64).range(1..)
)]
pub stream_idle_timeout_secs: u64,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

impl Config {
Expand All @@ -58,9 +79,19 @@ impl Config {
default_api_key: None,
debug: false,
enable_cors: false,
request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
stream_idle_timeout_secs: DEFAULT_STREAM_IDLE_TIMEOUT_SECS,
}
}

pub fn request_timeout(&self) -> Duration {
Duration::from_secs(self.request_timeout_secs)
}

pub fn stream_idle_timeout(&self) -> Duration {
Duration::from_secs(self.stream_idle_timeout_secs)
}

/// Builder-style method to set the API key
pub fn with_api_key(mut self, api_key: String) -> Self {
self.default_api_key = Some(api_key);
Expand All @@ -78,6 +109,18 @@ impl Config {
self.enable_cors = enable_cors;
self
}

/// Builder-style method to set the backend request timeout
pub fn with_request_timeout_secs(mut self, request_timeout_secs: u64) -> Self {
self.request_timeout_secs = request_timeout_secs;
self
}

/// Builder-style method to set the streaming idle timeout
pub fn with_stream_idle_timeout_secs(mut self, stream_idle_timeout_secs: u64) -> Self {
self.stream_idle_timeout_secs = stream_idle_timeout_secs;
self
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -114,3 +157,57 @@ impl OpenAIError {
Self::new(message, "server_error")
}
}

#[cfg(test)]
mod tests {
use super::*;
use clap::{error::ErrorKind, Parser};

#[test]
fn config_new_uses_timeout_defaults() {
let config = Config::new(
"127.0.0.1".to_string(),
8080,
"https://enclave.trymaple.ai".to_string(),
);

assert_eq!(config.request_timeout_secs, DEFAULT_REQUEST_TIMEOUT_SECS);
assert_eq!(
config.stream_idle_timeout_secs,
DEFAULT_STREAM_IDLE_TIMEOUT_SECS
);
assert_eq!(
config.request_timeout(),
Duration::from_secs(DEFAULT_REQUEST_TIMEOUT_SECS)
);
assert_eq!(
config.stream_idle_timeout(),
Duration::from_secs(DEFAULT_STREAM_IDLE_TIMEOUT_SECS)
);
}

#[test]
fn timeout_builder_methods_override_defaults() {
let config = Config::new(
"127.0.0.1".to_string(),
8080,
"https://enclave.trymaple.ai".to_string(),
)
.with_request_timeout_secs(45)
.with_stream_idle_timeout_secs(15);

assert_eq!(config.request_timeout(), Duration::from_secs(45));
assert_eq!(config.stream_idle_timeout(), Duration::from_secs(15));
}

#[test]
fn timeout_cli_values_must_be_positive() {
let request_timeout_error =
Config::try_parse_from(["maple-proxy", "--request-timeout-secs", "0"]).unwrap_err();
assert_eq!(request_timeout_error.kind(), ErrorKind::ValueValidation);

let stream_idle_timeout_error =
Config::try_parse_from(["maple-proxy", "--stream-idle-timeout-secs", "0"]).unwrap_err();
assert_eq!(stream_idle_timeout_error.kind(), ErrorKind::ValueValidation);
}
}
Loading
Loading