mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix: allow empty provider secrets to attempt requests
This commit is contained in:
@@ -82,3 +82,116 @@ pub(crate) async fn resolve_candidate_oauth_auth(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use aether_provider_transport::snapshot::{
|
||||||
|
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||||
|
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||||
|
};
|
||||||
|
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
|
||||||
|
|
||||||
|
use super::{prepare_header_authenticated_candidate, OauthPreparationContext};
|
||||||
|
use crate::ai_pipeline::PlannerAppState;
|
||||||
|
|
||||||
|
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||||
|
GatewayProviderTransportSnapshot {
|
||||||
|
provider: GatewayProviderTransportProvider {
|
||||||
|
id: "provider-1".to_string(),
|
||||||
|
name: "provider".to_string(),
|
||||||
|
provider_type: "custom".to_string(),
|
||||||
|
website: None,
|
||||||
|
is_active: true,
|
||||||
|
keep_priority_on_conversion: false,
|
||||||
|
enable_format_conversion: false,
|
||||||
|
concurrent_limit: None,
|
||||||
|
max_retries: None,
|
||||||
|
proxy: None,
|
||||||
|
request_timeout_secs: None,
|
||||||
|
stream_first_byte_timeout_secs: None,
|
||||||
|
config: None,
|
||||||
|
},
|
||||||
|
endpoint: GatewayProviderTransportEndpoint {
|
||||||
|
id: "endpoint-1".to_string(),
|
||||||
|
provider_id: "provider-1".to_string(),
|
||||||
|
api_format: "openai:chat".to_string(),
|
||||||
|
api_family: Some("openai".to_string()),
|
||||||
|
endpoint_kind: Some("chat".to_string()),
|
||||||
|
is_active: true,
|
||||||
|
base_url: "https://example.test".to_string(),
|
||||||
|
header_rules: None,
|
||||||
|
body_rules: None,
|
||||||
|
max_retries: None,
|
||||||
|
custom_path: None,
|
||||||
|
config: None,
|
||||||
|
format_acceptance_config: None,
|
||||||
|
proxy: None,
|
||||||
|
},
|
||||||
|
key: GatewayProviderTransportKey {
|
||||||
|
id: "key-1".to_string(),
|
||||||
|
provider_id: "provider-1".to_string(),
|
||||||
|
name: "key".to_string(),
|
||||||
|
auth_type: "api_key".to_string(),
|
||||||
|
is_active: true,
|
||||||
|
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||||
|
auth_type_by_format: None,
|
||||||
|
allowed_models: None,
|
||||||
|
capabilities: None,
|
||||||
|
rate_multipliers: None,
|
||||||
|
global_priority_by_format: None,
|
||||||
|
expires_at_unix_secs: None,
|
||||||
|
proxy: None,
|
||||||
|
fingerprint: None,
|
||||||
|
decrypted_api_key: String::new(),
|
||||||
|
decrypted_auth_config: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sample_candidate() -> SchedulerMinimalCandidateSelectionCandidate {
|
||||||
|
SchedulerMinimalCandidateSelectionCandidate {
|
||||||
|
provider_id: "provider-1".to_string(),
|
||||||
|
provider_name: "provider".to_string(),
|
||||||
|
provider_type: "custom".to_string(),
|
||||||
|
provider_priority: 1,
|
||||||
|
endpoint_id: "endpoint-1".to_string(),
|
||||||
|
endpoint_api_format: "openai:chat".to_string(),
|
||||||
|
key_id: "key-1".to_string(),
|
||||||
|
key_name: "key".to_string(),
|
||||||
|
key_auth_type: "api_key".to_string(),
|
||||||
|
key_internal_priority: 1,
|
||||||
|
key_global_priority_for_format: None,
|
||||||
|
key_capabilities: None,
|
||||||
|
model_id: "model-1".to_string(),
|
||||||
|
global_model_id: "global-model-1".to_string(),
|
||||||
|
global_model_name: "gpt-test".to_string(),
|
||||||
|
selected_provider_model_name: "gpt-test-upstream".to_string(),
|
||||||
|
mapping_matched_model: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn header_auth_preparation_allows_empty_auth_value() {
|
||||||
|
let state = crate::AppState::new().expect("state should build");
|
||||||
|
let transport = sample_transport();
|
||||||
|
let candidate = sample_candidate();
|
||||||
|
|
||||||
|
let prepared = prepare_header_authenticated_candidate(
|
||||||
|
PlannerAppState::new(&state),
|
||||||
|
&transport,
|
||||||
|
&candidate,
|
||||||
|
Some(("authorization".to_string(), String::new())),
|
||||||
|
OauthPreparationContext {
|
||||||
|
trace_id: "trace-empty-auth",
|
||||||
|
api_format: "openai:chat",
|
||||||
|
operation: "test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("empty auth value should still prepare the candidate");
|
||||||
|
|
||||||
|
assert_eq!(prepared.auth_header, "authorization");
|
||||||
|
assert_eq!(prepared.auth_value, "");
|
||||||
|
assert_eq!(prepared.mapped_model, "gpt-test-upstream");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -242,22 +242,23 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
|||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut provider_request_headers = BTreeMap::from([
|
let mut provider_request_headers =
|
||||||
("content-type".to_string(), "application/json".to_string()),
|
BTreeMap::from([("content-type".to_string(), "application/json".to_string())]);
|
||||||
(auth_header.clone(), auth_value.clone()),
|
if !auth_header.trim().is_empty() && !auth_value.trim().is_empty() {
|
||||||
]);
|
provider_request_headers.insert(auth_header.clone(), auth_value.clone());
|
||||||
|
}
|
||||||
if uses_vertex_query_auth {
|
if uses_vertex_query_auth {
|
||||||
provider_request_headers.remove("x-goog-api-key");
|
provider_request_headers.remove("x-goog-api-key");
|
||||||
}
|
}
|
||||||
let protected_headers = if uses_vertex_query_auth {
|
let protected_headers = if uses_vertex_query_auth || auth_value.trim().is_empty() {
|
||||||
&["content-type"][..]
|
vec!["content-type"]
|
||||||
} else {
|
} else {
|
||||||
&[auth_header.as_str(), "content-type"][..]
|
vec![auth_header.as_str(), "content-type"]
|
||||||
};
|
};
|
||||||
if !crate::provider_transport::apply_local_header_rules(
|
if !crate::provider_transport::apply_local_header_rules(
|
||||||
&mut provider_request_headers,
|
&mut provider_request_headers,
|
||||||
transport.endpoint.header_rules.as_ref(),
|
transport.endpoint.header_rules.as_ref(),
|
||||||
protected_headers,
|
&protected_headers,
|
||||||
&provider_request_body,
|
&provider_request_body,
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -95,8 +95,12 @@ pub async fn build_standard_models_fetch_execution_plan(
|
|||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
"Rust models fetch auth resolution is not supported for this key".to_string()
|
"Rust models fetch auth resolution is not supported for this key".to_string()
|
||||||
})?;
|
})?;
|
||||||
protected_headers.push(auth_header_name.clone());
|
insert_non_empty_auth_header(
|
||||||
headers.insert(auth_header_name.clone(), auth_header_value.clone());
|
&mut headers,
|
||||||
|
&mut protected_headers,
|
||||||
|
&auth_header_name,
|
||||||
|
&auth_header_value,
|
||||||
|
);
|
||||||
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
||||||
ensure_upstream_auth_header(&mut headers, &auth_header_name, &auth_header_value);
|
ensure_upstream_auth_header(&mut headers, &auth_header_name, &auth_header_value);
|
||||||
} else {
|
} else {
|
||||||
@@ -181,6 +185,7 @@ pub async fn build_gemini_cli_load_code_assist_plan(
|
|||||||
) -> Result<ExecutionPlan, String> {
|
) -> Result<ExecutionPlan, String> {
|
||||||
let authorization = resolve_bearer_or_oauth_header_auth(runtime, transport)
|
let authorization = resolve_bearer_or_oauth_header_auth(runtime, transport)
|
||||||
.await?
|
.await?
|
||||||
|
.filter(|(_, value)| !value.trim().is_empty())
|
||||||
.ok_or_else(|| "GeminiCLI loadCodeAssist requires bearer or OAuth auth".to_string())?;
|
.ok_or_else(|| "GeminiCLI loadCodeAssist requires bearer or OAuth auth".to_string())?;
|
||||||
|
|
||||||
let mut headers = BTreeMap::from([
|
let mut headers = BTreeMap::from([
|
||||||
@@ -188,8 +193,13 @@ pub async fn build_gemini_cli_load_code_assist_plan(
|
|||||||
("accept-encoding".to_string(), "identity".to_string()),
|
("accept-encoding".to_string(), "identity".to_string()),
|
||||||
("content-type".to_string(), "application/json".to_string()),
|
("content-type".to_string(), "application/json".to_string()),
|
||||||
]);
|
]);
|
||||||
headers.insert(authorization.0.clone(), authorization.1.clone());
|
let mut protected_headers = Vec::new();
|
||||||
let protected_headers = vec![authorization.0];
|
insert_non_empty_auth_header(
|
||||||
|
&mut headers,
|
||||||
|
&mut protected_headers,
|
||||||
|
&authorization.0,
|
||||||
|
&authorization.1,
|
||||||
|
);
|
||||||
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
||||||
|
|
||||||
build_execution_plan(
|
build_execution_plan(
|
||||||
@@ -225,8 +235,7 @@ pub async fn build_vertex_models_fetch_execution_plan(
|
|||||||
let mut headers = standard_models_fetch_headers(api_format, &transport.provider.provider_type);
|
let mut headers = standard_models_fetch_headers(api_format, &transport.provider.provider_type);
|
||||||
let mut protected_headers = Vec::<String>::new();
|
let mut protected_headers = Vec::<String>::new();
|
||||||
if let Some((name, value)) = auth_header {
|
if let Some((name, value)) = auth_header {
|
||||||
protected_headers.push(name.clone());
|
insert_non_empty_auth_header(&mut headers, &mut protected_headers, &name, &value);
|
||||||
headers.insert(name.clone(), value.clone());
|
|
||||||
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
headers = apply_fetch_header_rules(transport, headers, &protected_headers)?;
|
||||||
ensure_upstream_auth_header(&mut headers, &name, &value);
|
ensure_upstream_auth_header(&mut headers, &name, &value);
|
||||||
} else {
|
} else {
|
||||||
@@ -472,6 +481,22 @@ fn append_query_param(mut url: String, key: &str, value: &str) -> String {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_non_empty_auth_header(
|
||||||
|
headers: &mut BTreeMap<String, String>,
|
||||||
|
protected_headers: &mut Vec<String>,
|
||||||
|
name: &str,
|
||||||
|
value: &str,
|
||||||
|
) {
|
||||||
|
let name = name.trim();
|
||||||
|
let value = value.trim();
|
||||||
|
if name.is_empty() || value.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected_headers.push(name.to_string());
|
||||||
|
headers.insert(name.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use aether_contracts::{ExecutionPlan, ExecutionResult, ProxySnapshot};
|
use aether_contracts::{ExecutionPlan, ExecutionResult, ProxySnapshot};
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ pub fn resolve_local_openai_bearer_auth(
|
|||||||
}
|
}
|
||||||
let secret = resolved_local_secret(transport)?;
|
let secret = resolved_local_secret(transport)?;
|
||||||
|
|
||||||
Some(("authorization".to_string(), format!("Bearer {secret}")))
|
Some(("authorization".to_string(), bearer_auth_value(secret)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_local_standard_auth(
|
pub fn resolve_local_standard_auth(
|
||||||
@@ -247,7 +247,7 @@ pub fn resolve_local_standard_auth(
|
|||||||
|
|
||||||
match auth_type.as_str() {
|
match auth_type.as_str() {
|
||||||
"api_key" => Some(("x-api-key".to_string(), secret.to_string())),
|
"api_key" => Some(("x-api-key".to_string(), secret.to_string())),
|
||||||
"bearer" => Some(("authorization".to_string(), format!("Bearer {secret}"))),
|
"bearer" => Some(("authorization".to_string(), bearer_auth_value(secret))),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,7 @@ pub fn resolve_local_gemini_auth(
|
|||||||
|
|
||||||
match auth_type.as_str() {
|
match auth_type.as_str() {
|
||||||
"api_key" => Some(("x-goog-api-key".to_string(), secret.to_string())),
|
"api_key" => Some(("x-goog-api-key".to_string(), secret.to_string())),
|
||||||
"bearer" => Some(("authorization".to_string(), format!("Bearer {secret}"))),
|
"bearer" => Some(("authorization".to_string(), bearer_auth_value(secret))),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,21 @@ pub(crate) fn resolve_local_auth_type_for_transport_format(
|
|||||||
|
|
||||||
fn resolved_local_secret(transport: &GatewayProviderTransportSnapshot) -> Option<&str> {
|
fn resolved_local_secret(transport: &GatewayProviderTransportSnapshot) -> Option<&str> {
|
||||||
let secret = transport.key.decrypted_api_key.trim();
|
let secret = transport.key.decrypted_api_key.trim();
|
||||||
(!secret.is_empty() && secret != PLACEHOLDER_API_KEY).then_some(secret)
|
if !secret.is_empty() && secret != PLACEHOLDER_API_KEY {
|
||||||
|
Some(secret)
|
||||||
|
} else if transport.key.decrypted_auth_config.is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bearer_auth_value(secret: &str) -> String {
|
||||||
|
if secret.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("Bearer {secret}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -465,16 +479,31 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_standard_auth_rejects_placeholder_secret() {
|
fn local_standard_auth_keeps_header_shape_for_placeholder_secret() {
|
||||||
assert!(resolve_local_standard_auth(&sample_transport()).is_none());
|
assert_eq!(
|
||||||
|
resolve_local_standard_auth(&sample_transport()),
|
||||||
|
Some(("authorization".to_string(), String::new()))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn local_standard_auth_rejects_empty_secret() {
|
fn local_standard_auth_keeps_header_shape_for_empty_secret() {
|
||||||
let mut transport = sample_transport();
|
let mut transport = sample_transport();
|
||||||
transport.key.auth_type = "api_key".to_string();
|
transport.key.auth_type = "api_key".to_string();
|
||||||
transport.key.decrypted_api_key = String::new();
|
transport.key.decrypted_api_key = String::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
resolve_local_standard_auth(&transport),
|
||||||
|
Some(("x-api-key".to_string(), String::new()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_standard_auth_defers_to_auth_config_when_raw_secret_is_empty() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.key.decrypted_auth_config =
|
||||||
|
Some(r#"{"access_token":"cached-token"}"#.to_string());
|
||||||
|
|
||||||
assert!(resolve_local_standard_auth(&transport).is_none());
|
assert!(resolve_local_standard_auth(&transport).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ use super::super::snapshot::GatewayProviderTransportSnapshot;
|
|||||||
use super::super::supports_local_oauth_request_auth_resolution;
|
use super::super::supports_local_oauth_request_auth_resolution;
|
||||||
|
|
||||||
pub fn supports_local_claude_code_auth(transport: &GatewayProviderTransportSnapshot) -> bool {
|
pub fn supports_local_claude_code_auth(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||||
resolve_local_standard_auth(transport).is_some()
|
resolve_local_standard_auth(transport).is_some_and(|(_, value)| !value.trim().is_empty())
|
||||||
|| supports_local_oauth_request_auth_resolution(transport)
|
|| supports_local_oauth_request_auth_resolution(transport)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user