mod credential;
mod deployment;
pub mod native;
pub mod openai;
mod policy;
mod record;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize};
pub use credential::{
ExternalSecretReference, ProviderCredentialSourceView, ProviderCredentialState,
ProviderCredentialView, ProviderCredentialWrite, ProviderCredentialWriteSource,
};
pub use deployment::{
DeploymentAccess, ProviderAdapter, ProviderAuth, ProviderAuthHeader, ProviderDeployment,
VertexLocation,
};
pub use policy::{
FALLBACK_HEADER, FallbackRule, FallbackScope, GatewayBudget, GatewayBudgetPeriod,
GatewayCaptureMode, GatewayCapturePolicy, GatewayCapturePolicyWrite, GatewayFallbackOverride,
GatewayFallbackPolicy, GatewayGovernancePolicy, GatewayLimit, GatewayLimitSubject,
GatewayModelPricing, GatewayPayloadField, GatewayPolicySubject, GatewayPolicyTarget,
GatewayPriceRate, MAX_FALLBACK_HEADER_BYTES, MAX_FALLBACK_JSON_BYTES, UnknownCostPolicy,
};
pub use record::{
GATEWAY_JSON_MAX_BYTES, GatewayAccountingEntryId, GatewayAccountingEntryV1,
GatewayAttemptSpanFieldsV1, GatewayBudgetReservationId, GatewayCallId, GatewayCallOutcome,
GatewayCallPayloadV1, GatewayPayloadObjectRefV1, GatewayUsageAmount,
};
use crate::ids::{ModelId, ProviderDeploymentName};
const MAX_TEXT_LEN: usize = 128;
pub const BUILTIN_PROVIDER_IDS: [&str; 5] = ["openai", "anthropic", "gemini", "vertex", "jev"];
pub const RUN_HEADER: &str = "wyrd-run-id";
pub const CARD_UID_HEADER: &str = "wyrd-card-uid";
pub const PROMPT_PROVIDER_HEADER: &str = "wyrd-prompt-provider";
pub const MAX_RUN_HEADER_BYTES: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("gateway field {field} is invalid: {reason}")]
pub struct GatewayContractError {
pub field: String,
pub reason: &'static str,
}
impl GatewayContractError {
#[must_use]
pub fn new(field: impl Into<String>, reason: &'static str) -> Self {
Self {
field: field.into(),
reason,
}
}
}
fn validate_text(field: &str, value: &str) -> Result<(), GatewayContractError> {
if value.is_empty() || value.len() > MAX_TEXT_LEN || value.chars().any(char::is_control) {
return Err(GatewayContractError::new(
field,
"must be 1..=128 bytes without control characters",
));
}
Ok(())
}
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct ModelRef {
pub deployment: ProviderDeploymentName,
pub model: ModelId,
}
impl ModelRef {
pub fn from_projection(value: &str) -> Result<Self, GatewayContractError> {
let (deployment, model) = value
.split_once('/')
.ok_or_else(|| GatewayContractError::new("model", "must be <deployment>/<model>"))?;
Ok(Self {
deployment: ProviderDeploymentName::new(deployment).map_err(|_| {
GatewayContractError::new("model", "deployment is not a valid name")
})?,
model: ModelId::new(model)
.map_err(|_| GatewayContractError::new("model", "model is not a valid model id"))?,
})
}
}
impl fmt::Display for ModelRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.deployment, self.model)
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
schemars::JsonSchema,
)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum GatewayOperation {
ChatCompletions,
Responses,
Embeddings,
Images,
Audio,
Batches,
SystemOne,
}
impl GatewayOperation {
#[must_use]
pub const fn streams(self) -> bool {
matches!(self, Self::ChatCompletions | Self::Responses)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct GatewayDecimal(String);
impl GatewayDecimal {
pub fn new(value: &str) -> Result<Self, GatewayContractError> {
let invalid = || GatewayContractError::new("decimal", "must be a non-negative decimal");
if value.is_empty() || value.len() > 64 {
return Err(invalid());
}
let (integer, fraction) = value.split_once('.').unwrap_or((value, ""));
let digits = |part: &str| part.bytes().all(|byte| byte.is_ascii_digit());
if integer.is_empty() || !digits(integer) || !digits(fraction) {
return Err(invalid());
}
if value.contains('.') && fraction.is_empty() {
return Err(invalid());
}
let integer = integer.trim_start_matches('0');
let fraction = fraction.trim_end_matches('0');
let integer = if integer.is_empty() { "0" } else { integer };
Ok(Self(if fraction.is_empty() {
integer.to_owned()
} else {
format!("{integer}.{fraction}")
}))
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.0 == "0"
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for GatewayDecimal {
type Err = GatewayContractError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for GatewayDecimal {
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, Hash, Serialize, schemars::JsonSchema)]
#[cfg_attr(feature = "server", derive(utoipa::ToSchema))]
#[serde(transparent)]
pub struct CurrencyCode(String);
impl CurrencyCode {
pub fn new(value: &str) -> Result<Self, GatewayContractError> {
if value.len() == 3 && value.bytes().all(|byte| byte.is_ascii_uppercase()) {
Ok(Self(value.to_owned()))
} else {
Err(GatewayContractError::new(
"currency",
"must be an ISO-4217 code of three uppercase letters",
))
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for CurrencyCode {
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)
}
}
#[cfg(test)]
mod tests {
use super::{CurrencyCode, GatewayDecimal, GatewayOperation, ModelRef};
use serde_json::json;
#[test]
fn model_projection_splits_on_first_slash() {
let model =
ModelRef::from_projection("support-gateway/org/model-v3").expect("projection parses");
assert_eq!(model.deployment.as_str(), "support-gateway");
assert_eq!(model.model.as_str(), "org/model-v3");
assert_eq!(model.to_string(), "support-gateway/org/model-v3");
assert_eq!(
serde_json::to_value(&model).expect("model serializes"),
json!({"deployment": "support-gateway", "model": "org/model-v3"})
);
assert!(ModelRef::from_projection("no-slash").is_err());
assert!(ModelRef::from_projection("Bad/model").is_err());
assert!(ModelRef::from_projection("openai/").is_err());
}
#[test]
fn operations_are_closed() {
assert_eq!(
serde_json::to_value(GatewayOperation::ChatCompletions).expect("serializes"),
json!("chat_completions")
);
assert_eq!(
serde_json::to_value(GatewayOperation::SystemOne).expect("serializes"),
json!("system_one")
);
assert!(!GatewayOperation::SystemOne.streams());
assert!(serde_json::from_value::<GatewayOperation>(json!("rerank")).is_err());
}
#[test]
fn decimals_normalize_and_reject_invalid_text() {
assert_eq!(
GatewayDecimal::new("001.500").expect("valid").as_str(),
"1.5"
);
assert_eq!(GatewayDecimal::new("0.000").expect("valid").as_str(), "0");
assert!(GatewayDecimal::new("0").expect("valid").is_zero());
for invalid in ["", "-1", "1.", ".5", "1e3", "1,5", "NaN"] {
assert!(GatewayDecimal::new(invalid).is_err(), "{invalid}");
}
assert!(CurrencyCode::new("USD").is_ok());
assert!(CurrencyCode::new("usd").is_err());
}
}