use axum::body::Body;
use axum::extract::State;
use axum::http::{HeaderMap, HeaderName, StatusCode};
use axum::response::Response;
use crate::crypto::SecretBinding;
use crate::error::ApiError;
use crate::state::EdgeState;
pub const PROXY_TOKEN_HEADER: &str = "x-brain-edge-llm-token";
pub const PROXY_TOKEN_ENV: &str = "BRAIN_EDGE_LLM_PROXY_TOKEN";
const OPENAI_BASE: &str = "https://api.openai.com";
const ANTHROPIC_BASE: &str = "https://api.anthropic.com";
pub const PROVIDER_OPENAI: &str = "openai";
pub const PROVIDER_ANTHROPIC: &str = "anthropic";
pub const BROKER_PATH_PREFIX: &str = "/llm/ns";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Upstream {
OpenAi,
Anthropic,
}
impl Upstream {
pub const ALL: [Self; 2] = [Self::OpenAi, Self::Anthropic];
#[must_use]
pub const fn provider(self) -> &'static str {
match self {
Self::OpenAi => PROVIDER_OPENAI,
Self::Anthropic => PROVIDER_ANTHROPIC,
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|u| u.provider() == name)
}
const fn base(self) -> &'static str {
match self {
Self::OpenAi => OPENAI_BASE,
Self::Anthropic => ANTHROPIC_BASE,
}
}
}
fn token_matches(presented: &str, configured: &str) -> bool {
use subtle::ConstantTimeEq;
presented.len() == configured.len()
&& bool::from(presented.as_bytes().ct_eq(configured.as_bytes()))
}
fn authenticate(headers: &HeaderMap) -> Result<(), ApiError> {
authenticate_with(std::env::var(PROXY_TOKEN_ENV).ok().as_deref(), headers)
}
fn presented_token(headers: &HeaderMap) -> &str {
if let Some(v) = headers
.get(HeaderName::from_static(PROXY_TOKEN_HEADER))
.and_then(|v| v.to_str().ok())
{
return v.trim();
}
headers
.get(axum::http::header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.map(str::trim)
.and_then(|v| {
v.strip_prefix("Bearer ")
.or_else(|| v.strip_prefix("bearer "))
})
.map(str::trim)
.unwrap_or_default()
}
fn authenticate_with(configured: Option<&str>, headers: &HeaderMap) -> Result<(), ApiError> {
let Some(configured) = configured.map(str::trim).filter(|s| !s.is_empty()) else {
return Err(ApiError::not_implemented(
"the BYOK LLM broker is not enabled on this edge",
));
};
if !token_matches(presented_token(headers), configured) {
return Err(ApiError::unauthorized("broker token missing or invalid"));
}
Ok(())
}
pub async fn openai_chat_completions(
State(state): State<EdgeState>,
axum::extract::Path(namespace): axum::extract::Path<String>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, ApiError> {
proxy(
&state,
&headers,
&namespace,
Upstream::OpenAi,
"/v1/chat/completions",
body,
)
.await
}
pub async fn anthropic_messages(
State(state): State<EdgeState>,
axum::extract::Path(namespace): axum::extract::Path<String>,
headers: HeaderMap,
body: axum::body::Bytes,
) -> Result<Response, ApiError> {
proxy(
&state,
&headers,
&namespace,
Upstream::Anthropic,
"/v1/messages",
body,
)
.await
}
pub async fn resolve(
State(state): State<EdgeState>,
axum::extract::Path(namespace): axum::extract::Path<String>,
headers: HeaderMap,
) -> Result<axum::response::Json<crate::dto::llm_credential::ResolvedLlmDto>, ApiError> {
authenticate(&headers)?;
let store = state.llm_credentials().ok_or_else(|| {
ApiError::not_implemented("BYOK is not configured on this edge: no key-encryption key")
})?;
let record = store.get(&namespace).await?.ok_or_else(|| {
ApiError::llm_credential_missing(format!(
"no LLM provider key is registered for namespace {namespace}"
))
})?;
tracing::info!(
target: "brain_edge::llm_audit",
namespace = %namespace,
provider = %record.provider,
"BYOK: credential resolved for a namespace",
);
Ok(axum::response::Json(
crate::dto::llm_credential::ResolvedLlmDto {
provider: record.provider,
model: record.model,
},
))
}
#[allow(clippy::too_many_arguments)]
async fn proxy(
state: &EdgeState,
headers: &HeaderMap,
namespace: &str,
upstream: Upstream,
path: &str,
body: axum::body::Bytes,
) -> Result<Response, ApiError> {
authenticate(headers)?;
let store = state.llm_credentials().ok_or_else(|| {
ApiError::not_implemented("BYOK is not configured on this edge: no key-encryption key")
})?;
let record = store.get(namespace).await?.ok_or_else(|| {
ApiError::llm_credential_missing(format!(
"no LLM provider key is registered for namespace {namespace}"
))
})?;
if record.provider != upstream.provider() {
return Err(ApiError::conflict(format!(
"namespace {namespace} is registered for `{}`, not `{}`",
record.provider,
upstream.provider()
)));
}
let key = state.open_llm_key(
&record.sealed,
SecretBinding {
namespace,
provider: upstream.provider(),
},
)?;
let key = std::str::from_utf8(&key)
.map_err(|_| ApiError::internal("stored provider key is not valid UTF-8"))?;
tracing::info!(
target: "brain_edge::llm_audit",
namespace = %namespace,
provider = upstream.provider(),
fingerprint = %record.fingerprint,
"BYOK: provider key used for an upstream call",
);
let url = format!("{}{path}", upstream.base());
let client = reqwest::Client::new();
let mut req = client
.post(&url)
.header("content-type", "application/json")
.body(body.to_vec());
req = match upstream {
Upstream::OpenAi => req.header("authorization", format!("Bearer {key}")),
Upstream::Anthropic => req
.header("x-api-key", key)
.header("anthropic-version", anthropic_version(headers)),
};
let resp = req.send().await.map_err(|e| {
tracing::warn!(
namespace = %namespace,
provider = upstream.provider(),
error = %e,
"LLM upstream request failed",
);
ApiError::bad_gateway("LLM provider request failed")
})?;
let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
let content_type = resp
.headers()
.get(reqwest::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("application/json")
.to_string();
let stream = resp.bytes_stream();
let mut out = Response::builder()
.status(status)
.header("content-type", content_type);
if status.is_success() {
out = out.header("x-brain-llm-provider", upstream.provider());
}
out.body(Body::from_stream(stream))
.map_err(|e| ApiError::internal(format!("build proxy response: {e}")))
}
fn anthropic_version(headers: &HeaderMap) -> String {
headers
.get("anthropic-version")
.and_then(|v| v.to_str().ok())
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("2023-06-01")
.to_string()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyCheck {
Valid,
Rejected(String),
Unreachable(String),
}
fn classify_probe(status: u16) -> KeyCheck {
match status {
200..=299 => KeyCheck::Valid,
401 | 403 => KeyCheck::Rejected(format!("the provider rejected this key ({status})")),
other => KeyCheck::Unreachable(format!("provider returned {other} while verifying")),
}
}
pub async fn validate_key(upstream: Upstream, key: &str) -> KeyCheck {
let url = format!("{}/v1/models", upstream.base());
let client = reqwest::Client::new();
let req = match upstream {
Upstream::OpenAi => client.get(&url).bearer_auth(key),
Upstream::Anthropic => client
.get(&url)
.header("x-api-key", key)
.header("anthropic-version", "2023-06-01"),
};
match req.send().await {
Ok(resp) => classify_probe(resp.status().as_u16()),
Err(e) => {
tracing::warn!(
target: "brain_edge::llm_proxy",
provider = upstream.provider(),
error = %e,
"could not reach provider to verify key",
);
KeyCheck::Unreachable("could not reach the provider to verify this key".into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_comparison_rejects_wrong_and_accepts_right() {
assert!(token_matches("s3cret", "s3cret"));
assert!(!token_matches("s3cret", "s3creT"));
assert!(!token_matches("", "s3cret"));
assert!(!token_matches("s3cret", ""));
assert!(!token_matches("s3cret-longer", "s3cret"));
}
#[test]
fn upstream_bases_are_fixed() {
assert_eq!(Upstream::OpenAi.base(), "https://api.openai.com");
assert_eq!(Upstream::Anthropic.base(), "https://api.anthropic.com");
}
#[test]
fn provider_names_round_trip() {
for u in Upstream::ALL {
assert_eq!(Upstream::from_name(u.provider()), Some(u));
}
assert_eq!(Upstream::from_name("cohere"), None);
}
#[test]
fn the_broker_path_matches_what_brain_builds() {
assert_eq!(BROKER_PATH_PREFIX, "/llm/ns");
assert_eq!(
format!(
"{BROKER_PATH_PREFIX}/acme/{}/v1/chat/completions",
Upstream::OpenAi.provider()
),
"/llm/ns/acme/openai/v1/chat/completions"
);
assert_eq!(
format!(
"{BROKER_PATH_PREFIX}/acme/{}/v1/messages",
Upstream::Anthropic.provider()
),
"/llm/ns/acme/anthropic/v1/messages"
);
}
#[test]
fn anthropic_version_passes_through_or_defaults() {
let mut h = HeaderMap::new();
assert_eq!(anthropic_version(&h), "2023-06-01");
h.insert("anthropic-version", "2024-10-22".parse().unwrap());
assert_eq!(anthropic_version(&h), "2024-10-22");
h.insert("anthropic-version", " ".parse().unwrap());
assert_eq!(anthropic_version(&h), "2023-06-01", "blank falls back");
}
fn headers_with(token: &str) -> HeaderMap {
let mut h = HeaderMap::new();
if !token.is_empty() {
h.insert(PROXY_TOKEN_HEADER, token.parse().unwrap());
}
h
}
fn bearer_headers(token: &str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(
axum::http::header::AUTHORIZATION,
format!("Bearer {token}").parse().unwrap(),
);
h
}
#[test]
fn an_unconfigured_broker_is_501_not_401() {
for configured in [None, Some(""), Some(" ")] {
let err = authenticate_with(configured, &headers_with("t")).unwrap_err();
assert_eq!(err.status(), StatusCode::NOT_IMPLEMENTED);
}
}
#[test]
fn a_wrong_or_missing_token_is_rejected_identically() {
let missing = authenticate_with(Some("real"), &headers_with("")).unwrap_err();
let wrong = authenticate_with(Some("real"), &headers_with("nope")).unwrap_err();
assert_eq!(missing.status(), StatusCode::UNAUTHORIZED);
assert_eq!(wrong.status(), StatusCode::UNAUTHORIZED);
assert_eq!(missing.message(), wrong.message());
}
#[test]
fn a_valid_token_authenticates_via_either_channel() {
assert!(authenticate_with(Some("real"), &headers_with("real")).is_ok());
assert!(authenticate_with(Some("real"), &bearer_headers("real")).is_ok());
}
#[test]
fn the_explicit_header_wins_over_a_bearer() {
let mut h = bearer_headers("real");
h.insert(PROXY_TOKEN_HEADER, "wrong".parse().unwrap());
assert!(authenticate_with(Some("real"), &h).is_err());
}
#[test]
fn a_bearer_without_the_prefix_is_not_accepted() {
let mut h = HeaderMap::new();
h.insert(axum::http::header::AUTHORIZATION, "real".parse().unwrap());
assert!(authenticate_with(Some("real"), &h).is_err());
}
}
#[cfg(test)]
mod key_check_tests {
use super::*;
#[test]
fn only_auth_statuses_mean_the_key_is_bad() {
assert_eq!(classify_probe(200), KeyCheck::Valid);
assert_eq!(classify_probe(204), KeyCheck::Valid);
assert!(matches!(classify_probe(401), KeyCheck::Rejected(_)));
assert!(matches!(classify_probe(403), KeyCheck::Rejected(_)));
}
#[test]
fn transient_statuses_do_not_condemn_the_key() {
for status in [429, 500, 502, 503, 504] {
assert!(
matches!(classify_probe(status), KeyCheck::Unreachable(_)),
"{status} says nothing about the key"
);
}
}
#[test]
fn an_unexpected_status_is_unreachable_not_rejected() {
assert!(matches!(classify_probe(404), KeyCheck::Unreachable(_)));
assert!(matches!(classify_probe(418), KeyCheck::Unreachable(_)));
}
#[test]
fn no_check_message_can_contain_a_key() {
for c in [
classify_probe(401),
classify_probe(500),
classify_probe(200),
] {
let text = format!("{c:?}");
assert!(!text.contains("sk-"), "{text}");
}
}
}