diff --git a/Cargo.toml b/Cargo.toml index c73b00c..2c00731 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,28 @@ -[package] -name = "beyond-ai" -version = "0.1.0" +[workspace] +members = ["crates/*"] +# Edition-2024 members want the v3 feature resolver; a virtual workspace doesn't inherit it from a +# member, so it's declared here explicitly. +resolver = "3" + +[workspace.package] edition = "2024" license = "MIT" rust-version = "1.85" -description = "Beyond AI gateway — egress L7 proxy to LLM providers" - -[lib] -name = "beyond_ai" -path = "src/lib.rs" -[[bin]] -name = "beyond-ai" -path = "src/main.rs" - -# `[lints]` binds every target in the crate — lib, bin, tests, benches. That matters: a crate-level -# `#![deny(...)]` attribute only covers the unit it's written in, so the binary root (`main.rs`) -# would otherwise escape the library's denies. Declaring them here closes that gap structurally. -[lints.rust] +# Lints live at the workspace level so every crate root in the tree inherits them via +# `[lints] workspace = true`. That matters: a crate-level `#![deny(...)]` attribute only covers the +# unit it's written in, so a binary root (`main.rs`) would otherwise escape the library's denies. +# Declaring them here closes that gap structurally, for every member. +[workspace.lints.rust] unsafe_code = "forbid" unused_must_use = "deny" -# Panic surface: a stray `.unwrap()`/`.expect()`/`panic!`/`todo!` in a request path is a worker -# crash, not an error response. Deny them so a new one is a hard CI failure (mise `check:rs` runs -# clippy with `-D warnings`). These are clippy *restriction* lints (allow-by-default); naming them -# here turns them on. The handful of genuine boot-time invariants carry a local -# `#[allow(clippy::expect_used)]` with a SAFETY-style note; test/bench targets allow them wholesale -# at the file head (asserting a precondition with `.unwrap()` is the point of a test). -[lints.clippy] +# Panic surface: a stray `.unwrap()`/`.expect()`/`panic!`/`todo!` in a request or agent-loop path is +# a worker crash, not an error response. Deny them so a new one is a hard CI failure (clippy runs with +# `-D warnings`). These are clippy *restriction* lints (allow-by-default); naming them here turns them +# on. Genuine boot-time invariants carry a local `#[allow(clippy::expect_used)]` with a SAFETY note; +# test/bench targets allow them wholesale (asserting a precondition with `.unwrap()` is the point). +[workspace.lints.clippy] unwrap_used = "deny" expect_used = "deny" panic = "deny" @@ -35,63 +30,21 @@ todo = "deny" unimplemented = "deny" # Release builds wrap arithmetic silently by default; turn that into a panic so an overflow on a -# size/count never goes unnoticed. Negligible cost for a proxy (arithmetic isn't the bottleneck). +# size/count never goes unnoticed. Profiles only take effect at the workspace root. [profile.release] overflow-checks = true -[dependencies] -# slipstream is published — consume it from crates.io, aliased to `store` so the code's -# `use store::...` is unchanged. No path deps into the `beyond` repo: this crate builds standalone. -store = { package = "beyond-slipstream", version = "0.1.0" } - -pingora = { version = "0.8", default-features = false, features = ["rustls"] } -pingora-core = "0.8" -pingora-limits = "0.8" -pingora-proxy = "0.8" - -arc-swap = "1" -arrayvec = "0.7" -async-trait = "0.1" +# Shared dependency versions. Members opt in with `.workspace = true`, so the version is pinned +# once for the whole tree. Only deps used (or expected to be used) by more than one crate live here; +# crate-specific deps stay in that crate's manifest. +[workspace.dependencies] base64 = "0.22" bytes = "1" clap = { version = "4", features = ["derive", "env"] } -ed25519-dalek = "2.2" figment = { version = "0.10", features = ["toml", "env"] } -getrandom = "0.3" -# The types Pingora's `ServeHttp` trait speaks (`Response>`); pin to the same 1.x already in -# the tree via Pingora so the admin app can name them directly. -http = "1" -memchr = "2" -prometheus = "0.13" -rustls = { version = "0.23", default-features = false, features = ["ring"] } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } -zeroize = "1" - -[dev-dependencies] -# Bench harnesses. Best tool per job: `divan` for the unit micro-bench (it measures allocations -# natively via AllocProfiler, alongside timing); `criterion` for the e2e macro-bench (`async_tokio` -# drives the round-trips, and its saved-baseline comparison tracks latency/RPS over time). -criterion = { version = "0.5", features = ["async_tokio"] } -divan = "0.1" -http-body-util = "0.1" -# `http2` + hyper-util's `server-auto` let the mock upstream serve H1 *and* H2 on one TLS listener -# (protocol chosen by ALPN), so the concurrency bench can drive the gateway's H2 path. `rcgen` mints a -# throwaway self-signed cert for that listener; `tokio-rustls` terminates TLS in front of hyper. -hyper = { version = "1", features = ["server", "http1", "http2"] } -hyper-util = { version = "0.1", features = ["tokio", "server-auto"] } -rcgen = "0.13" -reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } -tokio-rustls = "0.26" - -[[bench]] -name = "unit" -harness = false - -[[bench]] -name = "e2e" -harness = false diff --git a/Dockerfile b/Dockerfile index c391b9a..2f3ba79 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,8 @@ # # Multi-stage build for the Beyond AI gateway (`beyond-ai`). # -# Built from this crate's root (it's a single standalone crate — no workspace, -# all deps come from crates.io): +# Built from the workspace root. The repo is a Cargo workspace; this image builds +# only the `beyond-ai` member (`crates/gateway`). All deps come from crates.io: # # docker build -t beyond-ai . # @@ -46,12 +46,12 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ cargo chef cook --release --recipe-path recipe.json -# Now copy the full source and build just the gateway binary. +# Now copy the full source and build just the gateway member of the workspace. COPY . . RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/usr/local/cargo/git \ --mount=type=cache,target=/app/target \ - cargo build --release --bin beyond-ai \ + cargo build --release -p beyond-ai --bin beyond-ai \ && cp /app/target/release/beyond-ai /usr/local/bin/beyond-ai # --------------------------------------------------------------------------- diff --git a/README.md b/README.md index e852c19..620c952 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Route LLM traffic through one internal proxy. Apps use their stock OpenAI or Ant ## Quick Start ```sh -cp config.example.toml config.toml +cp crates/gateway/config.example.toml config.toml # Set at minimum: signing_keys and one pool key -AI_POOL_KEY_OPENAI=sk-... cargo run --release +AI_POOL_KEY_OPENAI=sk-... cargo run -p beyond-ai --release ``` Point any OpenAI-wire SDK at `http://ai.internal` with a virtual key: diff --git a/ARCHITECTURE.md b/crates/gateway/ARCHITECTURE.md similarity index 100% rename from ARCHITECTURE.md rename to crates/gateway/ARCHITECTURE.md diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml new file mode 100644 index 0000000..4314b6f --- /dev/null +++ b/crates/gateway/Cargo.toml @@ -0,0 +1,77 @@ +[package] +name = "beyond-ai" +version = "0.1.0" +edition.workspace = true +license.workspace = true +rust-version.workspace = true +description = "Beyond AI gateway — egress L7 proxy to LLM providers" + +[lib] +name = "beyond_ai" +path = "src/lib.rs" + +[[bin]] +name = "beyond-ai" +path = "src/main.rs" + +# Inherit the workspace lint gates (`unsafe_code = "forbid"`, denied panic surface, …). They bind +# every target in this crate — lib, bin, tests, benches — see the rationale in the root manifest. +[lints] +workspace = true + +[dependencies] +# slipstream is published — consume it from crates.io, aliased to `store` so the code's +# `use store::...` is unchanged. No path deps into the `beyond` repo: this crate builds standalone. +store = { package = "beyond-slipstream", version = "0.1.0" } + +pingora = { version = "0.8", default-features = false, features = ["rustls"] } +pingora-core = "0.8" +pingora-limits = "0.8" +pingora-proxy = "0.8" + +arc-swap = "1" +arrayvec = "0.7" +async-trait = "0.1" +base64.workspace = true +bytes.workspace = true +clap.workspace = true +ed25519-dalek = "2.2" +figment.workspace = true +getrandom = "0.3" +# The types Pingora's `ServeHttp` trait speaks (`Response>`); pin to the same 1.x already in +# the tree via Pingora so the admin app can name them directly. +http = "1" +memchr = "2" +prometheus = "0.13" +rustls = { version = "0.23", default-features = false, features = ["ring"] } +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +zeroize = "1" + +[dev-dependencies] +# Bench harnesses. Best tool per job: `divan` for the unit micro-bench (it measures allocations +# natively via AllocProfiler, alongside timing); `criterion` for the e2e macro-bench (`async_tokio` +# drives the round-trips, and its saved-baseline comparison tracks latency/RPS over time). +criterion = { version = "0.5", features = ["async_tokio"] } +divan = "0.1" +http-body-util = "0.1" +# `http2` + hyper-util's `server-auto` let the mock upstream serve H1 *and* H2 on one TLS listener +# (protocol chosen by ALPN), so the concurrency bench can drive the gateway's H2 path. `rcgen` mints a +# throwaway self-signed cert for that listener; `tokio-rustls` terminates TLS in front of hyper. +hyper = { version = "1", features = ["server", "http1", "http2"] } +hyper-util = { version = "0.1", features = ["tokio", "server-auto"] } +rcgen = "0.13" +reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } +tokio-rustls = "0.26" + +[[bench]] +name = "unit" +harness = false + +[[bench]] +name = "e2e" +harness = false diff --git a/benches/e2e.rs b/crates/gateway/benches/e2e.rs similarity index 100% rename from benches/e2e.rs rename to crates/gateway/benches/e2e.rs diff --git a/benches/unit.rs b/crates/gateway/benches/unit.rs similarity index 100% rename from benches/unit.rs rename to crates/gateway/benches/unit.rs diff --git a/config.example.toml b/crates/gateway/config.example.toml similarity index 100% rename from config.example.toml rename to crates/gateway/config.example.toml diff --git a/examples/mint_dev_key.rs b/crates/gateway/examples/mint_dev_key.rs similarity index 96% rename from examples/mint_dev_key.rs rename to crates/gateway/examples/mint_dev_key.rs index fea1afb..e777396 100644 --- a/examples/mint_dev_key.rs +++ b/crates/gateway/examples/mint_dev_key.rs @@ -52,7 +52,10 @@ fn main() { let mut ring = Keyring::new(); ring.insert(KID, verifying_key); let recovered = ring.verify(&token).expect("dev token must verify"); - assert_eq!(recovered, vk, "verified identity must match minted identity"); + assert_eq!( + recovered, vk, + "verified identity must match minted identity" + ); println!("SIGNING_PUBKEY_B64={pubkey_b64}"); println!("KID={KID}"); diff --git a/src/admin.rs b/crates/gateway/src/admin.rs similarity index 100% rename from src/admin.rs rename to crates/gateway/src/admin.rs diff --git a/src/circuit_breaker.rs b/crates/gateway/src/circuit_breaker.rs similarity index 100% rename from src/circuit_breaker.rs rename to crates/gateway/src/circuit_breaker.rs diff --git a/src/config.rs b/crates/gateway/src/config.rs similarity index 100% rename from src/config.rs rename to crates/gateway/src/config.rs diff --git a/src/deny.rs b/crates/gateway/src/deny.rs similarity index 100% rename from src/deny.rs rename to crates/gateway/src/deny.rs diff --git a/src/doctor.rs b/crates/gateway/src/doctor.rs similarity index 100% rename from src/doctor.rs rename to crates/gateway/src/doctor.rs diff --git a/src/error.rs b/crates/gateway/src/error.rs similarity index 100% rename from src/error.rs rename to crates/gateway/src/error.rs diff --git a/src/key.rs b/crates/gateway/src/key.rs similarity index 100% rename from src/key.rs rename to crates/gateway/src/key.rs diff --git a/src/lib.rs b/crates/gateway/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/gateway/src/lib.rs diff --git a/src/main.rs b/crates/gateway/src/main.rs similarity index 100% rename from src/main.rs rename to crates/gateway/src/main.rs diff --git a/src/metrics.rs b/crates/gateway/src/metrics.rs similarity index 100% rename from src/metrics.rs rename to crates/gateway/src/metrics.rs diff --git a/src/peek.rs b/crates/gateway/src/peek.rs similarity index 100% rename from src/peek.rs rename to crates/gateway/src/peek.rs diff --git a/src/proxy.rs b/crates/gateway/src/proxy.rs similarity index 100% rename from src/proxy.rs rename to crates/gateway/src/proxy.rs diff --git a/src/ratelimit.rs b/crates/gateway/src/ratelimit.rs similarity index 100% rename from src/ratelimit.rs rename to crates/gateway/src/ratelimit.rs diff --git a/src/route.rs b/crates/gateway/src/route.rs similarity index 100% rename from src/route.rs rename to crates/gateway/src/route.rs diff --git a/src/secret.rs b/crates/gateway/src/secret.rs similarity index 100% rename from src/secret.rs rename to crates/gateway/src/secret.rs diff --git a/src/state.rs b/crates/gateway/src/state.rs similarity index 100% rename from src/state.rs rename to crates/gateway/src/state.rs diff --git a/src/store_watch.rs b/crates/gateway/src/store_watch.rs similarity index 100% rename from src/store_watch.rs rename to crates/gateway/src/store_watch.rs diff --git a/src/usage.rs b/crates/gateway/src/usage.rs similarity index 100% rename from src/usage.rs rename to crates/gateway/src/usage.rs diff --git a/tests/common/mod.rs b/crates/gateway/tests/common/mod.rs similarity index 100% rename from tests/common/mod.rs rename to crates/gateway/tests/common/mod.rs diff --git a/tests/e2e.rs b/crates/gateway/tests/e2e.rs similarity index 100% rename from tests/e2e.rs rename to crates/gateway/tests/e2e.rs diff --git a/tests/smoke.rs b/crates/gateway/tests/smoke.rs similarity index 100% rename from tests/smoke.rs rename to crates/gateway/tests/smoke.rs diff --git a/mise.toml b/mise.toml index afd10c0..16c137b 100644 --- a/mise.toml +++ b/mise.toml @@ -15,7 +15,7 @@ run = "cargo build --release" [tasks."ai:mint-dev-key"] description = "Mint a deterministic DEV-ONLY signing pubkey + bai_v1 token (fixed seed) for wiring up the dev config. Not for production." -run = "cargo run --example mint_dev_key" +run = "cargo run -p beyond-ai --example mint_dev_key" [tasks."check:rs"] run = "cargo clippy --all-targets -- -D warnings"