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
51 changes: 5 additions & 46 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 15 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
[package]
name = "maple-proxy"
version = "0.1.12"
version = "0.2.0"
edition = "2021"
authors = ["OpenSecret"]
description = "Lightweight OpenAI-compatible proxy server for Maple/OpenSecret TEE infrastructure"
license = "MIT"
repository = "https://github.com/OpenSecret/maple-proxy"
homepage = "https://github.com/OpenSecret/maple-proxy"
repository = "https://github.com/OpenSecretCloud/maple-proxy"
homepage = "https://github.com/OpenSecretCloud/maple-proxy"
keywords = ["openai", "proxy", "tee", "opensecret", "maple"]
categories = ["web-programming::http-server", "api-bindings"]
include = [
"/src/**",
"/examples/**",
"/tests/**",
"/Cargo.toml",
"/Cargo.lock",
"/README.md",
"/LICENSE",
]

[lib]
name = "maple_proxy"
Expand All @@ -19,13 +28,13 @@ name = "maple-proxy"
path = "src/main.rs"

[dependencies]
# OpenSecret SDK
opensecret = "3.3.0"
# OpenSecret SDK
opensecret = "3.4.0"

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

# Serialization
Expand All @@ -51,6 +60,3 @@ http = "1.0"

[dev-dependencies]
axum-test = "18.0.1"
tokio-test = "0.4"
tower = { version = "0.5.2", features = ["util"] }
serde_json = "1.0"
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# 🍁 Maple Proxy

A lightweight OpenAI-compatible proxy server for Maple/OpenSecret's TEE infrastructure. Works with **any** OpenAI client library while providing the security and privacy benefits of Trusted Execution Environment (TEE) processing.
A lightweight proxy for Maple/OpenSecret's OpenAI-compatible inference
endpoints, with the security and privacy benefits of Trusted Execution
Environment (TEE) processing.

## 🚀 Features

- **100% OpenAI Compatible** - Drop-in replacement for OpenAI API
- **OpenAI-Compatible Surface** - Models, chat completions, and embeddings endpoints
- **Secure TEE Processing** - All requests processed in secure enclaves
- **Streaming Support** - Full Server-Sent Events streaming for chat completions
- **Lossless Chat Parameters** - Provider-specific request fields pass through unchanged
- **Streaming and Non-Streaming** - Supports both chat completion response modes
- **Flexible Authentication** - Environment variables or per-request API keys
- **Zero Client Changes** - Works with existing OpenAI client code
- **Familiar Clients** - Point compatible OpenAI clients at the proxy base URL
- **Lightweight** - Minimal overhead, maximum performance
- **CORS Support** - Ready for web applications

Expand All @@ -30,7 +33,7 @@ Add to your `Cargo.toml`:
[dependencies]
maple-proxy = { git = "https://github.com/opensecretcloud/maple-proxy" }
# Or if published to crates.io:
# maple-proxy = "0.1.0"
# maple-proxy = "0.2.0"
```

## ⚙️ Configuration
Expand All @@ -40,7 +43,7 @@ Set environment variables or use command-line arguments:
```bash
# Environment Variables
export MAPLE_HOST=127.0.0.1 # Server host (default: 127.0.0.1)
export MAPLE_PORT=3000 # Server port (default: 3000)
export MAPLE_PORT=8080 # Server port (default: 8080)
export MAPLE_BACKEND_URL=http://localhost:3000 # Maple backend URL (prod: https://enclave.trymaple.ai)
export MAPLE_API_KEY=your-maple-api-key # Default API key (optional)
export MAPLE_DEBUG=true # Enable debug logging
Expand Down Expand Up @@ -70,7 +73,7 @@ You should see:
📋 Available endpoints:
GET /health - Health check
GET /v1/models - List available models
POST /v1/chat/completions - Create chat completions (streaming)
POST /v1/chat/completions - Create chat completions (streaming & non-streaming)
POST /v1/embeddings - Create embeddings
```

Expand All @@ -82,7 +85,7 @@ curl http://localhost:8080/v1/models \
-H "Authorization: Bearer YOUR_MAPLE_API_KEY"
```

#### Chat Completions (Streaming)
#### Chat Completions
```bash
curl -N http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer YOUR_MAPLE_API_KEY" \
Expand All @@ -96,7 +99,9 @@ curl -N http://localhost:8080/v1/chat/completions \
}'
```

**Note:** Maple currently only supports streaming responses.
Set `stream` to `true` for Server-Sent Events or `false` for one JSON response.
Additional provider-specific JSON fields are forwarded without being parsed or
rewritten by the proxy or Rust SDK.

#### Embeddings
```bash
Expand Down
26 changes: 13 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::Parser;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use std::{net::SocketAddr, time::Duration};

pub const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 300;
Expand Down Expand Up @@ -123,22 +123,22 @@ impl Config {
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIError {
pub error: OpenAIErrorDetails,
#[derive(Debug, Serialize)]
pub(crate) struct OpenAIError {
error: OpenAIErrorDetails,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenAIErrorDetails {
pub message: String,
#[derive(Debug, Serialize)]
struct OpenAIErrorDetails {
message: String,
#[serde(rename = "type")]
pub error_type: String,
pub param: Option<String>,
pub code: Option<String>,
error_type: String,
param: Option<String>,
code: Option<String>,
}

impl OpenAIError {
pub fn new(message: impl Into<String>, error_type: impl Into<String>) -> Self {
fn new(message: impl Into<String>, error_type: impl Into<String>) -> Self {
Self {
error: OpenAIErrorDetails {
message: message.into(),
Expand All @@ -149,11 +149,11 @@ impl OpenAIError {
}
}

pub fn authentication_error(message: impl Into<String>) -> Self {
pub(crate) fn authentication_error(message: impl Into<String>) -> Self {
Self::new(message, "invalid_request_error")
}

pub fn server_error(message: impl Into<String>) -> Self {
pub(crate) fn server_error(message: impl Into<String>) -> Self {
Self::new(message, "server_error")
}
}
Expand Down
19 changes: 11 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
pub mod config;
pub mod proxy;
mod config;
mod proxy;

pub use config::{Config, OpenAIError, OpenAIErrorDetails};
use proxy::{create_chat_completion, create_embeddings, health_check, list_models, ProxyState};
pub use config::Config;
use proxy::{health_check, proxy_openai_request, ProxyState};

use axum::{
extract::DefaultBodyLimit,
Expand All @@ -18,20 +18,23 @@ use tower_http::{
};
use tracing::Level;

pub const MAX_PROXY_REQUEST_BODY_BYTES: usize = 50 * 1024 * 1024;
const MAX_PROXY_REQUEST_BODY_BYTES: usize = 50 * 1024 * 1024;

/// Create the Axum application with the given configuration
pub fn create_app(config: Config) -> Router {
let state = Arc::new(ProxyState::new(config.clone()));
create_app_with_state(config, state)
}

pub(crate) fn create_app_with_state(config: Config, state: Arc<ProxyState>) -> Router {
let mut app = Router::new()
// Health check endpoints
.route("/health", get(health_check))
.route("/", get(health_check))
// OpenAI-compatible endpoints
.route("/v1/models", get(list_models))
.route("/v1/chat/completions", post(create_chat_completion))
.route("/v1/embeddings", post(create_embeddings))
.route("/v1/models", get(proxy_openai_request))
.route("/v1/chat/completions", post(proxy_openai_request))
.route("/v1/embeddings", post(proxy_openai_request))
.with_state(state)
.layer(
ServiceBuilder::new()
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async fn main() -> anyhow::Result<()> {
info!(
" Set MAPLE_API_KEY environment variable or provide Authorization: Bearer <key> header"
);
info!(" Compatible with any OpenAI client library!");
info!(" OpenAI-compatible clients can use this proxy as their base URL");
info!("");
info!("🔗 Example curl:");
info!(" curl http://{} \\", config.socket_addr()?);
Expand Down
Loading
Loading