use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ProviderRoute {
OpenAiResponses,
OpenAiChatCompletions,
OpenAiModels,
AnthropicMessages,
AnthropicCountTokens,
}
#[derive(Clone)]
pub(super) struct ProviderForwarding {
pub(super) source_route: ProviderRoute,
pub(super) authorization: crate::provider_auth::ProviderRequestAuthorization,
openai_auth_header: Option<String>,
anthropic_auth_header: Option<String>,
}
impl ProviderForwarding {
pub(super) fn new(
source_route: ProviderRoute,
authorization: crate::provider_auth::ProviderRequestAuthorization,
config: &crate::configuration::GatewayConfig,
) -> Self {
Self {
source_route,
authorization,
openai_auth_header: config.openai_auth_header.clone(),
anthropic_auth_header: config.anthropic_auth_header.clone(),
}
}
pub(super) fn configured_auth_header(&self, route: ProviderRoute) -> Option<&str> {
configured_auth_header(
route,
self.openai_auth_header.as_deref(),
self.anthropic_auth_header.as_deref(),
)
}
}
impl ProviderRoute {
pub(super) fn from_path(path: &str) -> Option<Self> {
match path {
"/responses" => Some(Self::OpenAiResponses),
"/v1/responses" => Some(Self::OpenAiResponses),
"/chat/completions" => Some(Self::OpenAiChatCompletions),
"/v1/chat/completions" => Some(Self::OpenAiChatCompletions),
"/models" => Some(Self::OpenAiModels),
"/v1/models" => Some(Self::OpenAiModels),
"/v1/messages" => Some(Self::AnthropicMessages),
"/v1/messages/count_tokens" => Some(Self::AnthropicCountTokens),
_ => None,
}
}
pub(super) fn from_dispatch_override(value: &str) -> Option<Self> {
match value {
"openai_chat"
| "openai_chat_completions"
| "openai.chat_completions"
| "/v1/chat/completions" => Some(Self::OpenAiChatCompletions),
"openai_responses" | "openai.responses" | "/v1/responses" => {
Some(Self::OpenAiResponses)
}
"openai_models" | "openai.models" | "/models" | "/v1/models" => {
Some(Self::OpenAiModels)
}
"anthropic_messages" | "anthropic.messages" | "/v1/messages" => {
Some(Self::AnthropicMessages)
}
"anthropic_count_tokens" | "anthropic.count_tokens" | "/v1/messages/count_tokens" => {
Some(Self::AnthropicCountTokens)
}
_ => None,
}
}
pub(super) const fn provider_surface(self) -> Option<ProviderSurface> {
match self {
Self::OpenAiResponses => Some(ProviderSurface::OpenAIResponses),
Self::OpenAiChatCompletions => Some(ProviderSurface::OpenAIChat),
Self::AnthropicMessages => Some(ProviderSurface::AnthropicMessages),
Self::AnthropicCountTokens | Self::OpenAiModels => None,
}
}
pub(super) const fn name(self) -> &'static str {
self.alignment_route().name()
}
pub(super) fn upstream_url(
self,
config: &crate::configuration::GatewayConfig,
path_and_query: &str,
) -> String {
let base = match self {
Self::OpenAiResponses | Self::OpenAiChatCompletions | Self::OpenAiModels => {
config.openai_base_url.as_str()
}
Self::AnthropicMessages | Self::AnthropicCountTokens => {
config.anthropic_base_url.as_str()
}
};
self.upstream_url_with_base(base, path_and_query)
}
pub(super) fn configured_auth_header(
self,
config: &crate::configuration::GatewayConfig,
) -> Option<&str> {
configured_auth_header(
self,
config.openai_auth_header.as_deref(),
config.anthropic_auth_header.as_deref(),
)
}
pub(super) fn upstream_url_with_base(self, base: &str, path_and_query: &str) -> String {
let base = base.trim_end_matches('/');
let path_and_query = match self {
Self::OpenAiResponses | Self::OpenAiChatCompletions | Self::OpenAiModels => {
normalize_openai_path_for_base(base, path_and_query)
}
_ => path_and_query.to_string(),
};
format!("{base}{path_and_query}")
}
pub(super) const fn alignment_route(self) -> GatewayRouteKind {
match self {
Self::OpenAiResponses => GatewayRouteKind::OpenAiResponses,
Self::OpenAiChatCompletions => GatewayRouteKind::OpenAiChatCompletions,
Self::OpenAiModels => GatewayRouteKind::OpenAiModels,
Self::AnthropicMessages => GatewayRouteKind::AnthropicMessages,
Self::AnthropicCountTokens => GatewayRouteKind::AnthropicCountTokens,
}
}
}
fn configured_auth_header<'a>(
route: ProviderRoute,
openai_auth_header: Option<&'a str>,
anthropic_auth_header: Option<&'a str>,
) -> Option<&'a str> {
match route {
ProviderRoute::OpenAiResponses
| ProviderRoute::OpenAiChatCompletions
| ProviderRoute::OpenAiModels => openai_auth_header,
ProviderRoute::AnthropicMessages | ProviderRoute::AnthropicCountTokens => {
anthropic_auth_header
}
}
}
pub(super) fn normalize_openai_path_for_base(base: &str, path_and_query: &str) -> String {
match (base.ends_with("/v1"), path_and_query.starts_with("/v1/")) {
(true, true) => path_and_query
.strip_prefix("/v1")
.expect("path was checked to start with /v1")
.to_string(),
(false, false) => format!("/v1{path_and_query}"),
_ => path_and_query.to_string(),
}
}
pub(super) fn gateway_upstream_url_override(
route: ProviderRoute,
headers: &HeaderMap,
path_and_query: &str,
allow_environment_provider_auth: bool,
config: &crate::configuration::GatewayConfig,
) -> Option<String> {
gateway_upstream_url_override_with_openai_key_state(
route,
headers,
path_and_query,
has_openai_replacement_auth(
route,
allow_environment_provider_auth,
route.configured_auth_header(config),
),
)
}
pub(super) fn gateway_upstream_url_override_with_openai_key_state(
route: ProviderRoute,
headers: &HeaderMap,
path_and_query: &str,
has_openai_replacement_key: bool,
) -> Option<String> {
alignment::gateway_upstream_url_override(
headers,
route.alignment_route(),
path_and_query,
has_openai_replacement_key,
)
}
pub(super) fn strip_replaceable_agent_auth_headers(
headers: &HeaderMap,
route: ProviderRoute,
allow_environment_provider_auth: bool,
configured_auth_header: Option<&str>,
) -> HeaderMap {
strip_replaceable_agent_auth_headers_with_openai_key_state(
headers,
route,
has_openai_replacement_auth(
route,
allow_environment_provider_auth,
configured_auth_header,
),
)
}
pub(super) fn strip_replaceable_agent_auth_headers_with_openai_key_state(
headers: &HeaderMap,
route: ProviderRoute,
has_openai_replacement_key: bool,
) -> HeaderMap {
alignment::gateway_forward_headers(headers, route.alignment_route(), has_openai_replacement_key)
}
pub(super) fn env_var_is_nonempty(name: &str) -> bool {
std::env::var(name)
.ok()
.filter(|value| !value.trim().is_empty())
.is_some()
}
fn has_openai_replacement_auth(
route: ProviderRoute,
allow_environment_provider_auth: bool,
configured_auth_header: Option<&str>,
) -> bool {
allow_environment_provider_auth
&& matches!(
route,
ProviderRoute::OpenAiResponses
| ProviderRoute::OpenAiChatCompletions
| ProviderRoute::OpenAiModels
)
&& (configured_auth_header.is_some() || env_var_is_nonempty("OPENAI_API_KEY"))
}
pub(super) fn gateway_session_id(
headers: &HeaderMap,
body: &Value,
route: ProviderRoute,
) -> Option<String> {
alignment::gateway_session_id(headers, body, route.alignment_route())
}
pub(super) fn gateway_subagent_id(
headers: &HeaderMap,
body: &Value,
route: ProviderRoute,
) -> Option<String> {
alignment::gateway_subagent_id(headers, body, route.alignment_route())
}
pub(super) fn gateway_identifier(
headers: &HeaderMap,
body: &Value,
header_name: &'static str,
body_paths: &[&[&str]],
) -> Option<String> {
alignment::gateway_identifier(headers, body, header_name, body_paths)
}