mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(gateway): harden provider request execution
Preserve exact request payloads and model client surface and API operation explicitly. Add Anthropic compatibility profiles, bounded stream commitment, and scoped OAuth retry behavior across provider transports.
This commit is contained in:
@@ -71,6 +71,20 @@ pub(super) fn extract_request_credentials(
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::control) fn resolve_gateway_credential_carrier(
|
||||
headers: &http::HeaderMap,
|
||||
uri: &Uri,
|
||||
auth_endpoint_signature: &str,
|
||||
) -> Option<GatewayCredentialCarrier> {
|
||||
extract_request_credentials(headers, uri, auth_endpoint_signature)
|
||||
.primary
|
||||
.map(|credential| match credential {
|
||||
GatewayPrimaryCredential::ProviderApiKey { carrier, .. }
|
||||
| GatewayPrimaryCredential::BearerToken { carrier, .. }
|
||||
| GatewayPrimaryCredential::CookieHeader { carrier, .. } => carrier,
|
||||
})
|
||||
}
|
||||
|
||||
fn has_trusted_gateway_marker(headers: &http::HeaderMap) -> bool {
|
||||
header_value_str(headers, crate::constants::GATEWAY_HEADER)
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -5,6 +5,7 @@ mod resolution;
|
||||
mod types;
|
||||
|
||||
pub(crate) use credentials::extract_requested_model;
|
||||
pub(super) use credentials::resolve_gateway_credential_carrier;
|
||||
pub(crate) use gate::{
|
||||
execution_plan_balance_capacity_rejection, request_model_local_rejection,
|
||||
should_buffer_request_for_local_auth, trusted_auth_local_rejection, GatewayLocalAuthRejection,
|
||||
@@ -14,3 +15,4 @@ pub(crate) use resolution::{
|
||||
GatewayAdminPrincipalContext, GatewayControlAuthContext,
|
||||
};
|
||||
pub(super) use resolution::{resolve_control_decision_auth, ControlDecisionAuthResolution};
|
||||
pub(crate) use types::GatewayCredentialCarrier;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum GatewayCredentialCarrier {
|
||||
pub(crate) enum GatewayCredentialCarrier {
|
||||
AuthorizationBearer,
|
||||
XApiKey,
|
||||
ApiKey,
|
||||
@@ -8,6 +8,26 @@ pub(super) enum GatewayCredentialCarrier {
|
||||
CookieHeader,
|
||||
}
|
||||
|
||||
impl GatewayCredentialCarrier {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::AuthorizationBearer => "authorization_bearer",
|
||||
Self::XApiKey => "x_api_key",
|
||||
Self::ApiKey => "api_key",
|
||||
Self::XGoogApiKey => "x_goog_api_key",
|
||||
Self::QueryKey => "query_key",
|
||||
Self::CookieHeader => "cookie_header",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn request_auth_channel(self) -> &'static str {
|
||||
match self {
|
||||
Self::AuthorizationBearer | Self::CookieHeader => "bearer_like",
|
||||
Self::XApiKey | Self::ApiKey | Self::XGoogApiKey | Self::QueryKey => "api_key",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(super) struct GatewayTrustedAuthHeaders {
|
||||
pub(super) user_id: String,
|
||||
|
||||
@@ -12,7 +12,7 @@ pub(crate) use auth::{
|
||||
refresh_execution_runtime_auth_context, request_model_local_rejection,
|
||||
resolve_execution_runtime_auth_context, should_buffer_request_for_local_auth,
|
||||
trusted_auth_local_rejection, GatewayAdminPrincipalContext, GatewayControlAuthContext,
|
||||
GatewayLocalAuthRejection,
|
||||
GatewayCredentialCarrier, GatewayLocalAuthRejection,
|
||||
};
|
||||
pub(crate) use execute::{allows_control_execute_emergency, maybe_execute_via_control};
|
||||
pub(crate) use management_token_permissions::{
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::{
|
||||
classified, classified_with_request_auth_channel, is_claude_cli_request, is_gemini_cli_request,
|
||||
is_gemini_models_route, is_gemini_operation_route, ClassifiedRoute,
|
||||
classified, classified_with_request_auth_channel, detect_claude_client_surface,
|
||||
is_gemini_cli_request, is_gemini_models_route, is_gemini_operation_route, ClassifiedRoute,
|
||||
};
|
||||
use crate::ai_serving::ApiOperation;
|
||||
|
||||
pub(super) fn classify_ai_public_route(
|
||||
method: &http::Method,
|
||||
@@ -76,27 +77,33 @@ pub(super) fn classify_ai_public_route(
|
||||
true,
|
||||
))
|
||||
} else if method == http::Method::POST && normalized_path == "/v1/messages/count_tokens" {
|
||||
Some(classified(
|
||||
"ai_public",
|
||||
"claude",
|
||||
"count_tokens",
|
||||
"claude:messages",
|
||||
false,
|
||||
))
|
||||
let request_auth_channel = claude_request_auth_channel(headers);
|
||||
Some(
|
||||
classified_with_request_auth_channel(
|
||||
"ai_public",
|
||||
"claude",
|
||||
"count_tokens",
|
||||
request_auth_channel,
|
||||
"claude:messages",
|
||||
true,
|
||||
)
|
||||
.with_client_surface(detect_claude_client_surface(headers))
|
||||
.with_api_operation(ApiOperation::ClaudeCountTokens),
|
||||
)
|
||||
} else if method == http::Method::POST && normalized_path == "/v1/messages" {
|
||||
let request_auth_channel = if is_claude_cli_request(headers) {
|
||||
"bearer_like"
|
||||
} else {
|
||||
"api_key"
|
||||
};
|
||||
Some(classified_with_request_auth_channel(
|
||||
"ai_public",
|
||||
"claude",
|
||||
"messages",
|
||||
request_auth_channel,
|
||||
"claude:messages",
|
||||
true,
|
||||
))
|
||||
let request_auth_channel = claude_request_auth_channel(headers);
|
||||
Some(
|
||||
classified_with_request_auth_channel(
|
||||
"ai_public",
|
||||
"claude",
|
||||
"messages",
|
||||
request_auth_channel,
|
||||
"claude:messages",
|
||||
true,
|
||||
)
|
||||
.with_client_surface(detect_claude_client_surface(headers))
|
||||
.with_api_operation(ApiOperation::ClaudeMessagesCreate),
|
||||
)
|
||||
} else if normalized_path.starts_with("/v1/videos") {
|
||||
Some(classified(
|
||||
"ai_public",
|
||||
@@ -178,6 +185,20 @@ pub(super) fn classify_ai_public_route(
|
||||
}
|
||||
}
|
||||
|
||||
fn claude_request_auth_channel(headers: &http::HeaderMap) -> &'static str {
|
||||
if crate::headers::header_value_str(headers, "x-api-key").is_some()
|
||||
|| crate::headers::header_value_str(headers, "api-key").is_some()
|
||||
{
|
||||
"api_key"
|
||||
} else if crate::headers::header_value_str(headers, http::header::AUTHORIZATION.as_str())
|
||||
.is_some_and(|value| value.trim().to_ascii_lowercase().starts_with("bearer "))
|
||||
{
|
||||
"bearer_like"
|
||||
} else {
|
||||
"api_key"
|
||||
}
|
||||
}
|
||||
|
||||
fn is_gemini_operation_method(method: &http::Method, normalized_path: &str) -> bool {
|
||||
method == http::Method::GET
|
||||
|| (method == http::Method::POST && normalized_path.ends_with(":cancel"))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use axum::http::Uri;
|
||||
|
||||
use crate::ai_serving::{ApiOperation, ClientSurface};
|
||||
use crate::headers::header_value_str;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
@@ -9,7 +10,10 @@ mod internal;
|
||||
mod oauth;
|
||||
mod public_support;
|
||||
|
||||
use super::auth::{resolve_control_decision_auth, ControlDecisionAuthResolution};
|
||||
use super::auth::{
|
||||
resolve_control_decision_auth, resolve_gateway_credential_carrier,
|
||||
ControlDecisionAuthResolution, GatewayCredentialCarrier,
|
||||
};
|
||||
use super::{GatewayAdminPrincipalContext, GatewayControlAuthContext, GatewayLocalAuthRejection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -19,6 +23,9 @@ pub(crate) struct GatewayControlDecision {
|
||||
pub(crate) route_class: Option<String>,
|
||||
pub(crate) route_family: Option<String>,
|
||||
pub(crate) route_kind: Option<String>,
|
||||
pub(crate) client_surface: Option<ClientSurface>,
|
||||
pub(crate) api_operation: Option<ApiOperation>,
|
||||
pub(crate) gateway_credential_carrier: Option<GatewayCredentialCarrier>,
|
||||
pub(crate) request_auth_channel: Option<String>,
|
||||
pub(crate) auth_endpoint_signature: Option<String>,
|
||||
pub(crate) execution_runtime_candidate: bool,
|
||||
@@ -42,6 +49,9 @@ impl GatewayControlDecision {
|
||||
route_class,
|
||||
route_family,
|
||||
route_kind,
|
||||
client_surface: None,
|
||||
api_operation: None,
|
||||
gateway_credential_carrier: None,
|
||||
request_auth_channel: None,
|
||||
auth_endpoint_signature,
|
||||
execution_runtime_candidate: false,
|
||||
@@ -80,6 +90,8 @@ pub(super) struct ClassifiedRoute {
|
||||
route_family: &'static str,
|
||||
route_kind: &'static str,
|
||||
request_auth_channel: Option<&'static str>,
|
||||
client_surface: Option<ClientSurface>,
|
||||
api_operation: Option<ApiOperation>,
|
||||
auth_endpoint_signature: String,
|
||||
execution_runtime_candidate: bool,
|
||||
}
|
||||
@@ -96,6 +108,8 @@ pub(super) fn classified(
|
||||
route_family,
|
||||
route_kind,
|
||||
request_auth_channel: None,
|
||||
client_surface: None,
|
||||
api_operation: None,
|
||||
auth_endpoint_signature: auth_endpoint_signature.into(),
|
||||
execution_runtime_candidate,
|
||||
}
|
||||
@@ -114,11 +128,25 @@ pub(super) fn classified_with_request_auth_channel(
|
||||
route_family,
|
||||
route_kind,
|
||||
request_auth_channel: Some(request_auth_channel),
|
||||
client_surface: None,
|
||||
api_operation: None,
|
||||
auth_endpoint_signature: auth_endpoint_signature.into(),
|
||||
execution_runtime_candidate,
|
||||
}
|
||||
}
|
||||
|
||||
impl ClassifiedRoute {
|
||||
pub(super) fn with_client_surface(mut self, client_surface: ClientSurface) -> Self {
|
||||
self.client_surface = Some(client_surface);
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn with_api_operation(mut self, api_operation: ApiOperation) -> Self {
|
||||
self.api_operation = Some(api_operation);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ClassifiedRoute {
|
||||
fn into_decision(self, public_path: String) -> GatewayControlDecision {
|
||||
GatewayControlDecision {
|
||||
@@ -127,6 +155,9 @@ impl ClassifiedRoute {
|
||||
route_class: Some(self.route_class.to_string()),
|
||||
route_family: Some(self.route_family.to_string()),
|
||||
route_kind: Some(self.route_kind.to_string()),
|
||||
client_surface: self.client_surface,
|
||||
api_operation: self.api_operation,
|
||||
gateway_credential_carrier: None,
|
||||
request_auth_channel: self.request_auth_channel.map(str::to_string),
|
||||
auth_endpoint_signature: Some(self.auth_endpoint_signature),
|
||||
execution_runtime_candidate: self.execution_runtime_candidate,
|
||||
@@ -183,7 +214,17 @@ pub(crate) fn classify_control_route(
|
||||
.or_else(|| internal::classify_internal_route(method, &normalized_path))
|
||||
.or_else(|| ai::classify_ai_public_route(method, &normalized_path, headers))?;
|
||||
|
||||
Some(classified.into_decision(normalized_path))
|
||||
let mut decision = classified.into_decision(normalized_path);
|
||||
if let Some(signature) = decision.auth_endpoint_signature.as_deref() {
|
||||
decision.gateway_credential_carrier =
|
||||
resolve_gateway_credential_carrier(headers, uri, signature);
|
||||
}
|
||||
if decision.route_family.as_deref() == Some("claude") {
|
||||
if let Some(carrier) = decision.gateway_credential_carrier {
|
||||
decision.request_auth_channel = Some(carrier.request_auth_channel().to_string());
|
||||
}
|
||||
}
|
||||
Some(decision)
|
||||
}
|
||||
|
||||
pub(super) fn detect_public_models_auth_signature(uri: &Uri, headers: &http::HeaderMap) -> String {
|
||||
@@ -220,11 +261,26 @@ pub(super) fn detect_public_models_auth_signature(uri: &Uri, headers: &http::Hea
|
||||
"openai:chat".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn is_claude_cli_request(headers: &http::HeaderMap) -> bool {
|
||||
let auth_header = header_value_str(headers, http::header::AUTHORIZATION.as_str())
|
||||
pub(super) fn detect_claude_client_surface(headers: &http::HeaderMap) -> ClientSurface {
|
||||
let user_agent = header_value_str(headers, http::header::USER_AGENT.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
auth_header.starts_with("bearer ")
|
||||
let x_app_is_cli = header_value_str(headers, "x-app")
|
||||
.is_some_and(|value| value.trim().eq_ignore_ascii_case("cli"));
|
||||
if user_agent.contains("claude-code")
|
||||
|| user_agent.contains("claude-cli")
|
||||
|| user_agent.contains("claude code")
|
||||
|| x_app_is_cli
|
||||
|| header_value_str(headers, "x-claude-code-session-id").is_some()
|
||||
{
|
||||
ClientSurface::ClaudeCode
|
||||
} else if user_agent.contains("anthropic/")
|
||||
|| header_value_str(headers, "x-stainless-lang").is_some()
|
||||
{
|
||||
ClientSurface::AnthropicSdk
|
||||
} else {
|
||||
ClientSurface::GenericCompatible
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_gemini_cli_request(headers: &http::HeaderMap) -> bool {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use aether_ai_formats::{ApiOperation, ClientSurface};
|
||||
use http::Uri;
|
||||
|
||||
use super::super::auth::GatewayCredentialCarrier;
|
||||
use super::{classify_control_route, headers};
|
||||
|
||||
#[test]
|
||||
fn classifies_claude_count_tokens_as_non_execution_runtime_public_route() {
|
||||
fn classifies_claude_count_tokens_as_execution_runtime_operation() {
|
||||
let headers = headers(&[("x-api-key", "sk-test")]);
|
||||
let uri: Uri = "/v1/messages/count_tokens"
|
||||
.parse()
|
||||
@@ -17,7 +19,11 @@ fn classifies_claude_count_tokens_as_non_execution_runtime_public_route() {
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("claude:messages")
|
||||
);
|
||||
assert!(!decision.is_execution_runtime_candidate());
|
||||
assert!(decision.is_execution_runtime_candidate());
|
||||
assert_eq!(
|
||||
decision.api_operation,
|
||||
Some(ApiOperation::ClaudeCountTokens)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -141,7 +147,7 @@ fn classifies_models_list_as_claude_when_headers_match() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_claude_messages_cli_when_bearer_without_api_key() {
|
||||
fn bearer_auth_does_not_imply_claude_code_client_surface() {
|
||||
let headers = headers(&[("authorization", "Bearer token-123")]);
|
||||
let uri: Uri = "/v1/messages".parse().expect("uri should parse");
|
||||
let decision =
|
||||
@@ -149,6 +155,10 @@ fn classifies_claude_messages_cli_when_bearer_without_api_key() {
|
||||
|
||||
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
||||
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
||||
assert_eq!(
|
||||
decision.client_surface,
|
||||
Some(ClientSurface::GenericCompatible)
|
||||
);
|
||||
assert_eq!(
|
||||
decision.request_auth_channel.as_deref(),
|
||||
Some("bearer_like")
|
||||
@@ -161,7 +171,7 @@ fn classifies_claude_messages_cli_when_bearer_without_api_key() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_claude_messages_cli_when_bearer_is_present_even_with_api_key() {
|
||||
fn claude_api_key_carrier_keeps_precedence_over_bearer() {
|
||||
let headers = headers(&[
|
||||
("authorization", "Bearer token-123"),
|
||||
("x-api-key", "sk-client"),
|
||||
@@ -172,9 +182,10 @@ fn classifies_claude_messages_cli_when_bearer_is_present_even_with_api_key() {
|
||||
|
||||
assert_eq!(decision.route_family.as_deref(), Some("claude"));
|
||||
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
|
||||
assert_eq!(decision.request_auth_channel.as_deref(), Some("api_key"));
|
||||
assert_eq!(
|
||||
decision.request_auth_channel.as_deref(),
|
||||
Some("bearer_like")
|
||||
decision.gateway_credential_carrier,
|
||||
Some(GatewayCredentialCarrier::XApiKey)
|
||||
);
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
@@ -183,6 +194,63 @@ fn classifies_claude_messages_cli_when_bearer_is_present_even_with_api_key() {
|
||||
assert!(decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_claude_code_independently_from_bearer_auth() {
|
||||
let headers = headers(&[
|
||||
("authorization", "Bearer token-123"),
|
||||
("user-agent", "Claude-Code/2.1.0"),
|
||||
]);
|
||||
let uri: Uri = "/v1/messages".parse().expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.client_surface, Some(ClientSurface::ClaudeCode));
|
||||
assert_eq!(
|
||||
decision.gateway_credential_carrier,
|
||||
Some(GatewayCredentialCarrier::AuthorizationBearer)
|
||||
);
|
||||
assert_eq!(
|
||||
decision.api_operation,
|
||||
Some(ApiOperation::ClaudeMessagesCreate)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_current_claude_cli_user_agent() {
|
||||
let headers = headers(&[
|
||||
("x-api-key", "sk-client"),
|
||||
("user-agent", "claude-cli/2.1.161 (external, cli)"),
|
||||
]);
|
||||
let uri: Uri = "/v1/messages".parse().expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.client_surface, Some(ClientSurface::ClaudeCode));
|
||||
assert_eq!(decision.request_auth_channel.as_deref(), Some("api_key"));
|
||||
assert_eq!(
|
||||
decision.gateway_credential_carrier,
|
||||
Some(GatewayCredentialCarrier::XApiKey)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_claude_code_from_explicit_x_app_signal() {
|
||||
let headers = headers(&[
|
||||
("x-api-key", "sk-client"),
|
||||
("user-agent", "rewritten-by-proxy"),
|
||||
("x-app", "cli"),
|
||||
]);
|
||||
let uri: Uri = "/v1/messages".parse().expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.client_surface, Some(ClientSurface::ClaudeCode));
|
||||
assert_eq!(
|
||||
decision.api_operation,
|
||||
Some(ApiOperation::ClaudeMessagesCreate)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_claude_messages_when_api_key_without_bearer() {
|
||||
let headers = headers(&[("x-api-key", "sk-client")]);
|
||||
|
||||
Reference in New Issue
Block a user