use std::collections::BTreeSet;
use serde::{Deserialize, Deserializer, Serialize};
use super::{BUILTIN_PROVIDER_IDS, GatewayContractError, GatewayOperation};
use crate::auth::AbsoluteUrl;
use crate::ids::{ModelId, ProviderCredentialName, ProviderDeploymentName, ProviderId};
const RESERVED_HEADERS: [&str; 20] = [
"authorization",
"proxy-authorization",
"proxy-authenticate",
"www-authenticate",
"host",
"forwarded",
"via",
"connection",
"keep-alive",
"transfer-encoding",
"te",
"trailer",
"upgrade",
"expect",
"content-length",
"content-type",
"cookie",
"set-cookie",
"x-real-ip",
"wyrd-request-id",
];
const RESERVED_HEADER_PREFIXES: [&str; 4] = ["proxy-", "x-forwarded-", "sec-", "wyrd-"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
pub enum ProviderAdapter {
#[serde(rename = "openai")]
OpenAi,
#[serde(rename = "anthropic")]
Anthropic,
#[serde(rename = "gemini")]
Gemini,
#[serde(rename = "vertex")]
Vertex {
project: VertexLocation,
location: VertexLocation,
},
#[serde(rename = "jev")]
Jev,
#[serde(rename = "openai_compatible")]
OpenAiCompatible {
base_url: AbsoluteUrl,
},
}
impl ProviderAdapter {
#[must_use]
pub const fn builtin_provider(&self) -> Option<&'static str> {
match self {
Self::OpenAi => Some("openai"),
Self::Anthropic => Some("anthropic"),
Self::Gemini => Some("gemini"),
Self::Vertex { .. } => Some("vertex"),
Self::Jev => Some("jev"),
Self::OpenAiCompatible { .. } => None,
}
}
#[must_use]
pub fn accepts_credential_provider(&self, provider: &ProviderId) -> bool {
match self.builtin_provider() {
Some(reserved) => reserved == provider.as_str(),
None => !BUILTIN_PROVIDER_IDS.contains(&provider.as_str()),
}
}
#[must_use]
pub const fn serves(&self, operation: GatewayOperation) -> bool {
match self {
Self::OpenAi | Self::OpenAiCompatible { .. } => {
!matches!(operation, GatewayOperation::SystemOne)
}
Self::Anthropic | Self::Gemini | Self::Vertex { .. } => {
matches!(operation, GatewayOperation::ChatCompletions)
}
Self::Jev => matches!(
operation,
GatewayOperation::SystemOne | GatewayOperation::ChatCompletions
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct VertexLocation(String);
impl VertexLocation {
pub fn new(value: &str) -> Result<Self, GatewayContractError> {
let valid = !value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_');
if valid {
Ok(Self(value.to_owned()))
} else {
Err(GatewayContractError::new(
"adapter.vertex",
"project and location must be 1..=128 ASCII alphanumerics, '-' or '_'",
))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for VertexLocation {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(&value).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ProviderAuth {
None,
Bearer {
credential: ProviderCredentialName,
},
ApiKeyHeader {
header: ProviderAuthHeader,
credential: ProviderCredentialName,
},
}
impl ProviderAuth {
#[must_use]
pub const fn credential(&self) -> Option<&ProviderCredentialName> {
match self {
Self::None => None,
Self::Bearer { credential } | Self::ApiKeyHeader { credential, .. } => Some(credential),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct ProviderAuthHeader(String);
impl ProviderAuthHeader {
pub fn new(value: &str) -> Result<Self, GatewayContractError> {
let token = !value.is_empty()
&& value.len() <= 128
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte));
if !token {
return Err(GatewayContractError::new(
"auth.header",
"must be a valid HTTP field name",
));
}
let lower = value.to_ascii_lowercase();
if RESERVED_HEADERS.contains(&lower.as_str())
|| RESERVED_HEADER_PREFIXES
.iter()
.any(|prefix| lower.starts_with(prefix))
{
return Err(GatewayContractError::new(
"auth.header",
"names a reserved or security-sensitive header",
));
}
Ok(Self(lower))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for ProviderAuthHeader {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::new(&value).map_err(serde::de::Error::custom)
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum DeploymentAccess {
#[default]
Open,
Granted,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct ProviderDeployment {
pub name: ProviderDeploymentName,
pub adapter: ProviderAdapter,
pub auth: ProviderAuth,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub models: Option<Vec<ModelId>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<BTreeSet<GatewayOperation>>,
#[serde(default)]
pub access: DeploymentAccess,
}
impl ProviderDeployment {
pub fn validate(&self) -> Result<(), GatewayContractError> {
if let Some(models) = &self.models {
if models.is_empty() {
return Err(GatewayContractError::new(
"models",
"must be omitted or list at least one model",
));
}
if models.iter().collect::<BTreeSet<_>>().len() != models.len() {
return Err(GatewayContractError::new(
"models",
"must not list a model twice",
));
}
}
if let Some(capabilities) = &self.capabilities {
if capabilities.is_empty() {
return Err(GatewayContractError::new(
"capabilities",
"must be omitted or declare at least one operation",
));
}
if capabilities
.iter()
.any(|operation| !self.adapter.serves(*operation))
{
return Err(GatewayContractError::new(
"capabilities",
"declares an operation the adapter does not serve",
));
}
}
Ok(())
}
#[must_use]
pub fn serves(&self, operation: GatewayOperation) -> bool {
self.adapter.serves(operation)
&& self
.capabilities
.as_ref()
.is_none_or(|capabilities| capabilities.contains(&operation))
}
#[must_use]
pub fn permits(&self, model: &ModelId) -> bool {
self.models
.as_ref()
.is_none_or(|models| models.contains(model))
}
}
#[cfg(test)]
mod tests {
use super::{DeploymentAccess, ProviderAuthHeader, ProviderDeployment};
use crate::gateway::GatewayOperation;
use crate::ids::{ModelId, ProviderId};
use serde_json::json;
fn decode(value: serde_json::Value) -> ProviderDeployment {
serde_json::from_value(value).expect("deployment decodes")
}
#[test]
fn deployment_round_trips_minimal_and_full_shapes() {
let minimal = decode(json!({
"name": "openai-primary",
"adapter": "openai",
"auth": {"bearer": {"credential": "openai-key"}},
}));
minimal.validate().expect("valid");
assert_eq!(minimal.access, DeploymentAccess::Open);
assert!(minimal.permits(&ModelId::new("any/model").expect("model")));
assert!(minimal.serves(GatewayOperation::Embeddings));
assert_eq!(
serde_json::to_value(&minimal).expect("encodes"),
json!({
"name": "openai-primary",
"adapter": "openai",
"auth": {"bearer": {"credential": "openai-key"}},
"access": "open",
})
);
let full = json!({
"name": "support-gateway",
"adapter": {"openai_compatible": {"base_url": "https://gateway.example/v1"}},
"auth": {"api_key_header": {"header": "x-api-key", "credential": "external-key"}},
"models": ["gpt-4o", "org/model-v3"],
"capabilities": ["chat_completions"],
"access": "granted",
});
let parsed = decode(full.clone());
parsed.validate().expect("valid");
assert_eq!(serde_json::to_value(&parsed).expect("encodes"), full);
assert!(parsed.permits(&ModelId::new("org/model-v3").expect("model")));
assert!(!parsed.permits(&ModelId::new("gpt-4.1").expect("model")));
assert!(!parsed.serves(GatewayOperation::Embeddings));
}
#[test]
fn invalid_shapes_fail_at_decode() {
let base = json!({"name": "primary", "adapter": "openai", "auth": "none"});
for (field, value) in [
("model", json!({"provider": "openai", "model": "gpt-4o"})),
("routing_weight", json!(1)),
("status", json!("active")),
("access", json!("public")),
] {
let mut document = base.clone();
document[field] = value;
assert!(
serde_json::from_value::<ProviderDeployment>(document).is_err(),
"{field}"
);
}
let mut bedrock = base;
bedrock["adapter"] = json!("bedrock");
assert!(serde_json::from_value::<ProviderDeployment>(bedrock).is_err());
for reserved in [
"Authorization",
"X-Forwarded-For",
"host",
"Proxy-Foo",
"bad header",
] {
assert!(ProviderAuthHeader::new(reserved).is_err(), "{reserved}");
}
}
#[test]
fn allowlists_and_capabilities_are_validated() {
let with = |field: &str, value: serde_json::Value, adapter: serde_json::Value| {
let mut document = json!({"name": "primary", "adapter": adapter, "auth": "none"});
document[field] = value;
decode(document).validate().map_err(|error| error.field)
};
assert_eq!(
with("models", json!([]), json!("openai")),
Err("models".to_owned())
);
assert_eq!(
with("models", json!(["gpt-4o", "gpt-4o"]), json!("openai")),
Err("models".to_owned())
);
assert_eq!(
with("capabilities", json!([]), json!("openai")),
Err("capabilities".to_owned())
);
assert_eq!(
with("capabilities", json!(["embeddings"]), json!("anthropic")),
Err("capabilities".to_owned()),
"anthropic serves no embeddings"
);
assert_eq!(
with("capabilities", json!(["system_one"]), json!("openai")),
Err("capabilities".to_owned()),
"only Jev serves systemone questions"
);
assert_eq!(
with(
"capabilities",
json!(["system_one", "chat_completions"]),
json!("jev")
),
Ok(())
);
let jev = decode(json!({"name": "jev", "adapter": "jev", "auth": "none"}));
assert!(jev.serves(GatewayOperation::SystemOne));
assert!(!jev.serves(GatewayOperation::Embeddings));
}
#[test]
fn credential_provider_follows_the_adapter() {
let provider = |name: &str| ProviderId::new(name).expect("provider");
let adapter = |value: serde_json::Value| {
decode(json!({"name": "anything", "adapter": value, "auth": "none"})).adapter
};
let openai = adapter(json!("openai"));
assert!(openai.accepts_credential_provider(&provider("openai")));
assert!(!openai.accepts_credential_provider(&provider("external")));
let vertex = adapter(json!({"vertex": {"project": "proj-1", "location": "us-central1"}}));
assert!(vertex.accepts_credential_provider(&provider("vertex")));
let compatible =
adapter(json!({"openai_compatible": {"base_url": "https://example.com/v1"}}));
assert!(compatible.accepts_credential_provider(&provider("external")));
for reserved in ["openai", "jev", "gemini"] {
assert!(!compatible.accepts_credential_provider(&provider(reserved)));
}
}
}