use std::fmt;
use std::time::Duration;
use crate::providers::{
AnthropicAdapterConfig, OpenAiAdapterConfig, OpenAiCompatibleAdapterConfig,
OpenAiCompatibleProtocol, ProviderAuth, ProviderTransport, SecretString,
};
use crate::client::{ProviderEndpointError, SippError, SippResult};
#[derive(Clone, PartialEq, Eq)]
pub struct ProviderSecret(String);
impl ProviderSecret {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub(crate) fn expose(&self) -> &str {
&self.0
}
fn as_gateway_secret(&self) -> SecretString {
SecretString::new(self.expose().to_string())
}
}
impl fmt::Debug for ProviderSecret {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ProviderSecret([redacted])")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderAuthConfig {
Bearer(ProviderSecret),
Header {
name: String,
value: ProviderSecret,
},
}
impl ProviderAuthConfig {
fn secrets(&self) -> Vec<String> {
match self {
Self::Bearer(secret) => vec![secret.expose().to_string()],
Self::Header { value, .. } => vec![value.expose().to_string()],
}
}
fn into_provider_auth(self) -> ProviderAuth {
match self {
Self::Bearer(secret) => ProviderAuth::Bearer(secret.as_gateway_secret()),
Self::Header { name, value } => ProviderAuth::Header {
name,
value: value.as_gateway_secret(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProviderEndpointConfig {
OpenAi(OpenAiProviderConfig),
Anthropic(AnthropicProviderConfig),
OpenAiCompatible(OpenAiCompatibleProviderConfig),
}
impl ProviderEndpointConfig {
pub fn openai(model: impl Into<String>, api_key: ProviderSecret) -> Self {
Self::OpenAi(OpenAiProviderConfig {
model: model.into(),
api_key,
base_url: None,
timeout: None,
})
}
pub fn anthropic(model: impl Into<String>, api_key: ProviderSecret) -> Self {
Self::Anthropic(AnthropicProviderConfig {
model: model.into(),
api_key,
base_url: None,
version: None,
timeout: None,
})
}
pub fn openai_compatible(
model: impl Into<String>,
base_url: impl Into<String>,
auth: ProviderAuthConfig,
) -> Self {
Self::OpenAiCompatible(OpenAiCompatibleProviderConfig {
model: model.into(),
base_url: base_url.into(),
auth,
static_headers: Vec::new(),
correlation_header: None,
timeout: None,
})
}
pub(crate) fn build(self) -> SippResult<(String, ProviderTransport, Vec<String>)> {
match self {
Self::OpenAi(config) => build_openai_provider(config),
Self::Anthropic(config) => build_anthropic_provider(config),
Self::OpenAiCompatible(config) => build_openai_compatible_provider(config),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiProviderConfig {
pub model: String,
pub api_key: ProviderSecret,
pub base_url: Option<String>,
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnthropicProviderConfig {
pub model: String,
pub api_key: ProviderSecret,
pub base_url: Option<String>,
pub version: Option<String>,
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenAiCompatibleProviderConfig {
pub model: String,
pub base_url: String,
pub auth: ProviderAuthConfig,
pub static_headers: Vec<(String, ProviderSecret)>,
pub correlation_header: Option<String>,
pub timeout: Option<Duration>,
}
fn build_openai_provider(
config: OpenAiProviderConfig,
) -> SippResult<(String, ProviderTransport, Vec<String>)> {
let model = normalize_model(config.model)?;
let secrets = vec![config.api_key.expose().to_string()];
let transport = ProviderTransport::openai(OpenAiAdapterConfig {
api_key: config.api_key.as_gateway_secret(),
base_url: config.base_url,
timeout: config.timeout,
})
.map_err(|error| {
SippError::Provider(ProviderEndpointError::from_provider_error(error, &secrets))
})?;
Ok((model, transport, secrets))
}
fn build_anthropic_provider(
config: AnthropicProviderConfig,
) -> SippResult<(String, ProviderTransport, Vec<String>)> {
let model = normalize_model(config.model)?;
let secrets = vec![config.api_key.expose().to_string()];
let transport = ProviderTransport::anthropic(AnthropicAdapterConfig {
api_key: config.api_key.as_gateway_secret(),
base_url: config.base_url,
version: config.version,
timeout: config.timeout,
})
.map_err(|error| {
SippError::Provider(ProviderEndpointError::from_provider_error(error, &secrets))
})?;
Ok((model, transport, secrets))
}
fn build_openai_compatible_provider(
config: OpenAiCompatibleProviderConfig,
) -> SippResult<(String, ProviderTransport, Vec<String>)> {
let model = normalize_model(config.model)?;
let mut secrets = config.auth.secrets();
secrets.extend(
config
.static_headers
.iter()
.map(|(_, value)| value.expose().to_string()),
);
let static_headers = config
.static_headers
.into_iter()
.map(|(name, value)| (name, value.expose().to_string()))
.collect();
let transport = ProviderTransport::openai_compatible(OpenAiCompatibleAdapterConfig {
base_url: config.base_url,
auth: config.auth.into_provider_auth(),
protocol: OpenAiCompatibleProtocol::OpenAiCompatible,
static_headers,
correlation_header: config.correlation_header,
timeout: config.timeout,
})
.map_err(|error| {
SippError::Provider(ProviderEndpointError::from_provider_error(error, &secrets))
})?;
Ok((model, transport, secrets))
}
fn normalize_model(model: String) -> SippResult<String> {
let trimmed = model.trim();
if trimmed.is_empty() {
Err(SippError::InvalidRequest(
"provider model must not be empty".to_string(),
))
} else if trimmed != model.as_str() {
Err(SippError::InvalidRequest(
"provider model must not contain surrounding whitespace".to_string(),
))
} else {
Ok(model)
}
}