#[derive(Debug, Clone, Copy)]
pub(crate) struct AnthropicBaseUrl {
known_bases: &'static [(&'static str, &'static str)],
openai_paths: &'static [&'static str],
anthropic_path: &'static str,
}
impl AnthropicBaseUrl {
pub(crate) const fn new(
known_bases: &'static [(&'static str, &'static str)],
openai_paths: &'static [&'static str],
anthropic_path: &'static str,
) -> Self {
Self {
known_bases,
openai_paths,
anthropic_path,
}
}
pub(crate) fn resolve_from_env(
self,
primary_env: &'static str,
fallback_env: &'static str,
) -> crate::client::ProviderClientResult<Option<String>> {
let primary = crate::client::optional_env_var(primary_env)?;
let fallback = crate::client::optional_env_var(fallback_env)?;
Ok(self.resolve(primary.as_deref(), fallback.as_deref()))
}
pub(crate) fn resolve(self, primary: Option<&str>, fallback: Option<&str>) -> Option<String> {
primary
.map(str::to_owned)
.or_else(|| fallback.and_then(|base_url| self.normalize(base_url)))
}
pub(crate) fn normalize(self, base_url: &str) -> Option<String> {
if base_url.contains("/anthropic") {
return Some(base_url.to_owned());
}
let trimmed = base_url.trim_end_matches('/');
if let Some((_, anthropic_base)) = self
.known_bases
.iter()
.find(|(openai_base, _)| *openai_base == trimmed)
{
return Some((*anthropic_base).to_owned());
}
let mut url = url::Url::parse(base_url).ok()?;
if !self.openai_path(url.path()) {
return None;
}
url.set_path(self.anthropic_path);
Some(url.to_string())
}
fn openai_path(self, path: &str) -> bool {
self.openai_paths.contains(&path)
}
}
macro_rules! impl_dual_dialect_provider {
(
ext = $ext:ident,
builder = $builder:ident,
anthropic_ext = $anthropic_ext:ident,
anthropic_builder = $anthropic_builder:ident,
client_input = $client_input:ty,
api_key_env = $api_key_env:literal,
base_url = $base_url:expr,
base_url_env = $base_url_env:literal,
anthropic_provider_name = $anthropic_name:literal,
anthropic_base_url = $anthropic_base_url:expr,
anthropic_base_url_env = $anthropic_base_url_env:literal $(,)?
) => {
#[derive(Debug, Default, Clone, Copy)]
pub struct $ext;
#[derive(Debug, Default, Clone, Copy)]
pub struct $builder;
#[derive(Debug, Default, Clone)]
pub struct $anthropic_builder {
anthropic: $crate::providers::anthropic::client::AnthropicBuilder,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct $anthropic_ext;
pub type Client<H = reqwest::Client> = $crate::client::Client<$ext, H>;
pub type ClientBuilder<H = $crate::markers::Missing> =
$crate::client::ClientBuilder<$builder, $crate::client::BearerAuth, H>;
pub type AnthropicClient<H = reqwest::Client> = $crate::client::Client<$anthropic_ext, H>;
pub type AnthropicClientBuilder<H = $crate::markers::Missing> = $crate::client::ClientBuilder<
$anthropic_builder,
$crate::providers::anthropic::client::AnthropicKey,
H,
>;
impl $crate::client::Provider for $ext {
type Builder = $builder;
const VERIFY_PATH: &'static str = "/models";
}
impl $crate::client::Provider for $anthropic_ext {
type Builder = $anthropic_builder;
const VERIFY_PATH: &'static str = "/v1/models";
}
impl $crate::client::DebugExt for $ext {}
impl $crate::client::DebugExt for $anthropic_ext {}
$crate::client::impl_capabilities!(
$anthropic_ext,
completion =
$crate::providers::anthropic::completion::GenericCompletionModel<$anthropic_ext, H>,
);
$crate::client::impl_default_provider_builder!(
$builder => $ext,
api_key = $crate::client::BearerAuth,
base_url = $base_url,
);
$crate::providers::anthropic::client::impl_anthropic_compatible_builder!(
$anthropic_builder => $anthropic_ext,
base_url = $anthropic_base_url,
);
impl $crate::providers::anthropic::completion::AnthropicCompatibleProvider
for $anthropic_ext
{
const PROVIDER_NAME: &'static str = $anthropic_name;
fn default_max_tokens(_model: &str) -> Option<u64> {
Some(4096)
}
}
$crate::client::impl_provider_client!(
Client,
input = $client_input,
api_key_env = $api_key_env,
base_url_env = $base_url_env,
);
$crate::client::impl_provider_client!(
AnthropicClient,
input = String,
api_key_env = $api_key_env,
base_url = ANTHROPIC_BASE_URLS.resolve_from_env($anthropic_base_url_env, $base_url_env)?,
);
};
}
pub(crate) use impl_dual_dialect_provider;
#[cfg(test)]
mod tests {
use super::AnthropicBaseUrl;
const RULE: AnthropicBaseUrl = AnthropicBaseUrl::new(
&[(
"https://api.example.com/v1",
"https://api.example.com/anthropic",
)],
&["/v1", "/v1/"],
"/anthropic",
);
#[test]
fn maps_known_and_custom_openai_bases() {
assert_eq!(
RULE.normalize("https://api.example.com/v1/").as_deref(),
Some("https://api.example.com/anthropic")
);
assert_eq!(
RULE.normalize("https://proxy.example.com/v1").as_deref(),
Some("https://proxy.example.com/anthropic")
);
}
#[test]
fn primary_wins_and_unknown_fallback_paths_are_ignored() {
assert_eq!(
RULE.resolve(
Some("https://primary.example.com/anthropic"),
Some("https://proxy.example.com/v1")
)
.as_deref(),
Some("https://primary.example.com/anthropic")
);
assert_eq!(
RULE.resolve(None, Some("https://proxy.example.com/api")),
None
);
}
}