diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider.rs index a097afe24..189de54b4 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider.rs @@ -46,8 +46,7 @@ use crate::ai_serving::transport::vertex::{ use crate::ai_serving::transport::{ apply_local_body_rules, apply_local_header_rules, build_passthrough_headers, ensure_upstream_auth_header, resolve_transport_execution_timeouts, - resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile, - LocalResolvedOAuthRequestAuth, + resolve_transport_proxy_snapshot_with_tunnel_affinity, LocalResolvedOAuthRequestAuth, }; use crate::ai_serving::{ collect_control_headers, ConversionMode, ExecutionStrategy, GatewayControlDecision, diff --git a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs index 6131307e9..e56737071 100644 --- a/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/payload.rs @@ -18,7 +18,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::{ append_execution_contract_fields_to_value, append_local_failover_policy_to_value, @@ -66,7 +66,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_ let proxy = state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport) .await; - let tls_profile = resolve_transport_tls_profile(&resolved.transport); + let transport_profile = resolve_transport_profile(&resolved.transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = build_request_trace_proxy_value(Some(&resolved.transport), proxy.as_ref()) @@ -173,7 +173,7 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_ provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy, - tls_profile, + transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), upstream_is_stream, report_kind: Some(report_kind.to_string()), diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs index 662f7089a..b783c27d1 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/files/decision.rs @@ -9,7 +9,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState}; use crate::{AiExecutionDecision, AppState}; @@ -62,7 +62,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat .app() .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await; - let tls_profile = resolve_transport_tls_profile(&transport); + let transport_profile = resolve_transport_profile(&transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) { extra_fields.insert("proxy".to_string(), proxy_value); @@ -147,7 +147,7 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), proxy, - tls_profile, + transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), upstream_is_stream: spec_metadata.require_streaming, report_kind: spec_metadata.report_kind.map(ToOwned::to_owned), diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs index 2398dc69b..c60e02d1a 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/image/decision.rs @@ -7,7 +7,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState}; use crate::{AiExecutionDecision, AppState}; @@ -53,7 +53,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat .app() .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await; - let tls_profile = resolve_transport_tls_profile(&transport); + let transport_profile = resolve_transport_profile(&transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) { extra_fields.insert("proxy".to_string(), proxy_value); @@ -126,7 +126,7 @@ pub(super) async fn maybe_build_local_openai_image_decision_payload_for_candidat provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy, - tls_profile, + transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), upstream_is_stream, report_kind: spec_metadata.report_kind.map(ToOwned::to_owned), diff --git a/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs b/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs index 793571b32..904ff40f8 100644 --- a/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs +++ b/apps/aether-gateway/src/ai_serving/planner/specialized/video/decision.rs @@ -7,7 +7,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::ai_serving::{ai_local_execution_contract_for_formats, PlannerAppState}; use crate::{AiExecutionDecision, AppState}; @@ -45,7 +45,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat .app() .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await; - let tls_profile = resolve_transport_tls_profile(&transport); + let transport_profile = resolve_transport_profile(&transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = build_request_trace_proxy_value(Some(&transport), proxy.as_ref()) { extra_fields.insert("proxy".to_string(), proxy_value); @@ -127,7 +127,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat .filter(|value| !value.is_empty()) .map(ToOwned::to_owned), proxy, - tls_profile, + transport_profile, timeouts: resolve_transport_execution_timeouts(&transport), upstream_is_stream: false, report_kind: spec_metadata.report_kind.map(ToOwned::to_owned), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs index b96820df2..efd7736cd 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/family/payload.rs @@ -16,7 +16,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::ai_serving::{ ai_local_execution_contract_for_formats, api_format_alias_matches, @@ -139,7 +139,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate( ), &resolved.transport, ); - let tls_profile = resolve_transport_tls_profile(&resolved.transport); + let transport_profile = resolve_transport_profile(&resolved.transport); let timeouts = resolve_transport_execution_timeouts(&resolved.transport); let super::request::LocalStandardCandidatePayloadParts { auth_header, @@ -181,7 +181,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate( provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy, - tls_profile, + transport_profile, timeouts, upstream_is_stream, report_kind: spec_metadata.report_kind.map(ToOwned::to_owned), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs index a6e123dcb..c7e0e0e40 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/chat/decision/payload.rs @@ -7,7 +7,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::{ append_execution_contract_fields_to_value, append_local_failover_policy_to_value, @@ -62,7 +62,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate let proxy = state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport) .await; - let tls_profile = resolve_transport_tls_profile(&resolved.transport); + let transport_profile = resolve_transport_profile(&resolved.transport); let timeouts = resolve_transport_execution_timeouts(&resolved.transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = @@ -171,7 +171,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy, - tls_profile, + transport_profile, timeouts, upstream_is_stream, report_kind: Some(report_kind), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs index 8509e3ab7..4a0492a50 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/stream.rs @@ -297,7 +297,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: true, report_kind: Some("openai_responses_stream_success".to_string()), @@ -393,7 +393,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: true, report_kind: Some("openai_chat_stream_success".to_string()), @@ -468,7 +468,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: true, report_kind: Some("openai_chat_stream_success".to_string()), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs index de6366317..90f9d8248 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/plan_builders/sync.rs @@ -284,7 +284,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: true, report_kind: Some("openai_responses_sync_success".to_string()), @@ -379,7 +379,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: Some("openai_chat_sync_success".to_string()), @@ -450,7 +450,7 @@ mod tests { provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: Some("openai_chat_sync_success".to_string()), diff --git a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs index d45d9aa54..915c11e56 100644 --- a/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs +++ b/apps/aether-gateway/src/ai_serving/planner/standard/openai/responses/decision/payload.rs @@ -11,7 +11,7 @@ use crate::ai_serving::planner::{ build_ai_execution_decision_response, AiExecutionDecisionResponseParts, }; use crate::ai_serving::transport::{ - resolve_transport_execution_timeouts, resolve_transport_tls_profile, + resolve_transport_execution_timeouts, resolve_transport_profile, }; use crate::{ append_execution_contract_fields_to_value, append_local_failover_policy_to_value, @@ -63,7 +63,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand let proxy = state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport) .await; - let tls_profile = resolve_transport_tls_profile(&resolved.transport); + let transport_profile = resolve_transport_profile(&resolved.transport); let timeouts = resolve_transport_execution_timeouts(&resolved.transport); let mut extra_fields = serde_json::Map::new(); if let Some(proxy_value) = @@ -194,7 +194,7 @@ pub(crate) async fn maybe_build_local_openai_responses_decision_payload_for_cand provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy, - tls_profile, + transport_profile, timeouts, upstream_is_stream, report_kind: spec_metadata.report_kind.map(ToOwned::to_owned), diff --git a/apps/aether-gateway/src/ai_serving/transport.rs b/apps/aether-gateway/src/ai_serving/transport.rs index 75798e7c0..714160672 100644 --- a/apps/aether-gateway/src/ai_serving/transport.rs +++ b/apps/aether-gateway/src/ai_serving/transport.rs @@ -70,11 +70,12 @@ pub(crate) use aether_provider_transport::{ request_conversion_enabled_for_transport, request_conversion_transport_supported, request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport, resolve_gemini_files_auth, resolve_openai_image_auth, resolve_same_format_provider_direct_auth, - resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot, - resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile, - resolve_video_create_auth, same_format_provider_transport_supported, - same_format_provider_transport_unsupported_reason, should_skip_upstream_passthrough_header, - should_try_same_format_provider_oauth_auth, supports_local_gemini_transport_with_network, + resolve_transport_execution_timeouts, resolve_transport_profile, + resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity, + resolve_transport_tls_profile, resolve_video_create_auth, + same_format_provider_transport_supported, same_format_provider_transport_unsupported_reason, + should_skip_upstream_passthrough_header, should_try_same_format_provider_oauth_auth, + supports_local_gemini_transport_with_network, supports_local_generic_oauth_request_auth_resolution, supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported, video_create_transport_unsupported_reason, CandidateTransportPolicyFacts, diff --git a/apps/aether-gateway/src/execution_runtime/fallback.rs b/apps/aether-gateway/src/execution_runtime/fallback.rs index d38691582..bc9a0741a 100644 --- a/apps/aether-gateway/src/execution_runtime/fallback.rs +++ b/apps/aether-gateway/src/execution_runtime/fallback.rs @@ -364,7 +364,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/apps/aether-gateway/src/execution_runtime/kiro_web_search.rs b/apps/aether-gateway/src/execution_runtime/kiro_web_search.rs index 8f61215dc..69b5b3fee 100644 --- a/apps/aether-gateway/src/execution_runtime/kiro_web_search.rs +++ b/apps/aether-gateway/src/execution_runtime/kiro_web_search.rs @@ -307,7 +307,7 @@ async fn execute_mcp_request( provider_api_format: plan.provider_api_format.clone(), model_name: plan.model_name.clone(), proxy: plan.proxy.clone(), - tls_profile: plan.tls_profile.clone(), + transport_profile: plan.transport_profile.clone(), timeouts: plan.timeouts.clone(), }; let result = DirectSyncExecutionRuntime::new() @@ -538,7 +538,7 @@ async fn discover_kiro_profile_arn_in_region( provider_api_format: "kiro:profiles".to_string(), model_name: Some("kiro-list-available-profiles".to_string()), proxy: plan.proxy.clone(), - tls_profile: plan.tls_profile.clone(), + transport_profile: plan.transport_profile.clone(), timeouts: plan.timeouts.clone(), }; let result = DirectSyncExecutionRuntime::new() @@ -1208,7 +1208,7 @@ mod tests { provider_api_format: "claude:messages".to_string(), model_name: Some("claude-sonnet-4.6".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/apps/aether-gateway/src/execution_runtime/server.rs b/apps/aether-gateway/src/execution_runtime/server.rs index 04202ddc4..0e0290c1f 100644 --- a/apps/aether-gateway/src/execution_runtime/server.rs +++ b/apps/aether-gateway/src/execution_runtime/server.rs @@ -352,6 +352,7 @@ impl IntoResponse for ExecutionRuntimeAppError { | ExecutionRuntimeTransportError::InvalidHeaderName(_) | ExecutionRuntimeTransportError::InvalidHeaderValue(_) | ExecutionRuntimeTransportError::InvalidProxy(_) + | ExecutionRuntimeTransportError::UnsupportedTransportProfile(_) | ExecutionRuntimeTransportError::BodyEncode(_), ) => StatusCode::BAD_REQUEST, ExecutionRuntimeServerError::Transport( @@ -422,7 +423,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(30_000), diff --git a/apps/aether-gateway/src/execution_runtime/stream/execution.rs b/apps/aether-gateway/src/execution_runtime/stream/execution.rs index 86dd112fb..e67c2995e 100644 --- a/apps/aether-gateway/src/execution_runtime/stream/execution.rs +++ b/apps/aether-gateway/src/execution_runtime/stream/execution.rs @@ -2504,7 +2504,7 @@ mod tests { provider_api_format: "openai:responses".into(), model_name: Some("gpt-5.4".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2625,7 +2625,7 @@ mod tests { provider_api_format: "openai:responses".into(), model_name: Some("gpt-5.4".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2733,7 +2733,7 @@ mod tests { provider_api_format: "openai:image".into(), model_name: Some("gpt-image-1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2822,7 +2822,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-5".into()), proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2951,7 +2951,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-5".into()), proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), diff --git a/apps/aether-gateway/src/execution_runtime/stream_pump.rs b/apps/aether-gateway/src/execution_runtime/stream_pump.rs index fd60dd57d..6cc26a9ff 100644 --- a/apps/aether-gateway/src/execution_runtime/stream_pump.rs +++ b/apps/aether-gateway/src/execution_runtime/stream_pump.rs @@ -777,7 +777,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-5".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -868,7 +868,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -986,7 +986,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1115,7 +1115,7 @@ mod tests { provider_api_format: "openai:image".to_string(), model_name: Some("gpt-image-1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1224,7 +1224,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-5".into()), proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1368,7 +1368,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-5".into()), proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), diff --git a/apps/aether-gateway/src/execution_runtime/tests.rs b/apps/aether-gateway/src/execution_runtime/tests.rs index 52e196fff..2ab76dfae 100644 --- a/apps/aether-gateway/src/execution_runtime/tests.rs +++ b/apps/aether-gateway/src/execution_runtime/tests.rs @@ -64,7 +64,7 @@ fn missing_exact_provider_request_payload(decision_kind: &str) -> AiExecutionDec provider_request_body_base64: None, content_type: Some("application/json".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: Some("openai_chat_sync_success".to_string()), @@ -806,7 +806,7 @@ fn bypasses_execution_runtime_for_codex_plan_variant() { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; diff --git a/apps/aether-gateway/src/execution_runtime/transport.rs b/apps/aether-gateway/src/execution_runtime/transport.rs index fc8a44190..e099899de 100644 --- a/apps/aether-gateway/src/execution_runtime/transport.rs +++ b/apps/aether-gateway/src/execution_runtime/transport.rs @@ -5,8 +5,9 @@ use std::io::Write; use std::time::{Duration, Instant}; use aether_contracts::{ - ExecutionPlan, ExecutionResult, ExecutionTelemetry, ProxySnapshot, ResponseBody, - EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER, + ExecutionPlan, ExecutionResult, ExecutionTelemetry, ProxySnapshot, ResolvedTransportProfile, + ResponseBody, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER, + TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_HTTP1_ONLY, }; use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation; use aether_http::{apply_http_client_config, HttpClientConfig}; @@ -102,6 +103,8 @@ pub(crate) enum ExecutionRuntimeTransportError { InvalidHeaderValue(String), #[error("invalid proxy configuration: {0}")] InvalidProxy(reqwest::Error), + #[error("unsupported transport profile backend: {0}")] + UnsupportedTransportProfile(String), #[error("failed to encode request body: {0}")] BodyEncode(serde_json::Error), #[error("failed to build HTTP client: {0}")] @@ -116,6 +119,9 @@ pub(crate) enum ExecutionRuntimeTransportError { #[derive(Debug, Serialize)] struct RelayRequestMeta { + provider_id: String, + endpoint_id: String, + key_id: String, method: String, url: String, headers: BTreeMap, @@ -124,6 +130,8 @@ struct RelayRequestMeta { follow_redirects: Option, #[serde(default, skip_serializing_if = "is_false")] http1_only: bool, + #[serde(skip_serializing_if = "Option::is_none")] + transport_profile: Option, } #[derive(Debug, Clone, Default)] @@ -531,12 +539,16 @@ fn build_direct_tunnel_request_meta( transport_controls: ExecutionTransportControls, ) -> tunnel_protocol::RequestMeta { tunnel_protocol::RequestMeta { + provider_id: Some(plan.provider_id.clone()), + endpoint_id: Some(plan.endpoint_id.clone()), + key_id: Some(plan.key_id.clone()), method: plan.method.clone(), url: plan.url.clone(), headers: header_map_to_string_map(headers).into_iter().collect(), timeout: resolve_relay_timeout_seconds(plan), follow_redirects: transport_controls.follow_redirects, http1_only: transport_controls.http1_only, + transport_profile: plan.transport_profile.clone(), } } @@ -577,7 +589,7 @@ async fn send_request( let client = build_client( plan.timeouts.as_ref(), plan.proxy.as_ref(), - plan.tls_profile.as_deref(), + plan.transport_profile.as_ref(), transport_controls, )?; let mut request = client.request(method, &plan.url); @@ -604,12 +616,16 @@ async fn send_via_tunnel_relay( let timeout_secs = resolve_relay_timeout_seconds(plan); let envelope = build_relay_envelope( RelayRequestMeta { + provider_id: plan.provider_id.clone(), + endpoint_id: plan.endpoint_id.clone(), + key_id: plan.key_id.clone(), method: method.as_str().to_string(), url: plan.url.clone(), headers: header_map_to_string_map(&headers), timeout: timeout_secs, follow_redirects: transport_controls.follow_redirects, http1_only: transport_controls.http1_only, + transport_profile: plan.transport_profile.clone(), }, &body_bytes, )?; @@ -856,14 +872,15 @@ fn resolve_local_tunnel_node_id(state: &AppState, proxy: Option<&ProxySnapshot>) fn build_client( timeouts: Option<&aether_contracts::ExecutionTimeouts>, proxy: Option<&ProxySnapshot>, - tls_profile: Option<&str>, + transport_profile: Option<&ResolvedTransportProfile>, transport_controls: ExecutionTransportControls, ) -> Result { + validate_reqwest_transport_profile(transport_profile)?; let mut builder = reqwest::Client::builder(); if transport_controls.follow_redirects != Some(true) { builder = builder.redirect(Policy::none()); } - if transport_controls.http1_only { + if transport_controls.http1_only || transport_profile_http1_only(transport_profile) { builder = builder.http1_only(); } let mut builder = apply_http_client_config( @@ -873,7 +890,10 @@ fn build_client( ..HttpClientConfig::default() }, ); - builder = apply_tls_profile(builder, tls_profile); + builder = apply_tls_profile( + builder, + transport_profile.map(|profile| profile.profile_id.as_str()), + ); if let Some(proxy_url) = resolve_proxy_url(proxy)? { let proxy = reqwest::Proxy::all(&proxy_url) .map_err(ExecutionRuntimeTransportError::InvalidProxy)?; @@ -884,11 +904,40 @@ fn build_client( .map_err(ExecutionRuntimeTransportError::ClientBuild) } +fn validate_reqwest_transport_profile( + transport_profile: Option<&ResolvedTransportProfile>, +) -> Result<(), ExecutionRuntimeTransportError> { + let Some(profile) = transport_profile else { + return Ok(()); + }; + if profile + .backend + .trim() + .eq_ignore_ascii_case(TRANSPORT_BACKEND_REQWEST_RUSTLS) + { + return Ok(()); + } + Err(ExecutionRuntimeTransportError::UnsupportedTransportProfile( + profile.backend.clone(), + )) +} + +fn transport_profile_http1_only(transport_profile: Option<&ResolvedTransportProfile>) -> bool { + transport_profile + .map(|profile| { + profile + .http_mode + .trim() + .eq_ignore_ascii_case(TRANSPORT_HTTP_MODE_HTTP1_ONLY) + }) + .unwrap_or(false) +} + fn apply_tls_profile( builder: reqwest::ClientBuilder, - tls_profile: Option<&str>, + profile_id: Option<&str>, ) -> reqwest::ClientBuilder { - let profile = normalize_tls_profile(tls_profile); + let profile = normalize_tls_profile(profile_id); if profile.is_none() { return builder; } @@ -1136,7 +1185,7 @@ mod tests { use std::sync::Arc; use aether_contracts::{ - ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody, + ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody, ResolvedTransportProfile, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER, }; use aether_data::repository::proxy_nodes::{ @@ -1154,7 +1203,8 @@ mod tests { use super::{ build_client, execute_sync_plan, record_manual_proxy_request_failure, record_manual_proxy_request_outcome, record_manual_proxy_request_success, - record_manual_proxy_stream_error, DirectSyncExecutionRuntime, ExecutionTransportControls, + record_manual_proxy_stream_error, DirectSyncExecutionRuntime, + ExecutionRuntimeTransportError, ExecutionTransportControls, }; use crate::constants::{ EXECUTION_RUNTIME_LOOP_GUARD_HEADER, EXECUTION_RUNTIME_LOOP_GUARD_VIA_TOKEN, @@ -1346,7 +1396,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1394,7 +1444,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(manual_proxy_snapshot("manual-node-1")), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1438,7 +1488,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(manual_proxy_snapshot("manual-node-1")), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1482,7 +1532,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(manual_proxy_snapshot("manual-node-1")), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1526,7 +1576,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(manual_proxy_snapshot("manual-node-1")), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1570,7 +1620,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(manual_proxy_snapshot("manual-node-1")), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1616,7 +1666,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: None, proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -1696,7 +1746,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: Some(tunnel_proxy_snapshot(format!("http://{addr}"))), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1748,7 +1798,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: Some(tunnel_proxy_snapshot("http://127.0.0.1:1".to_string())), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1897,7 +1947,7 @@ mod tests { provider_api_format: "provider_ops:verify".into(), model_name: Some("verify-auth".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -1976,7 +2026,7 @@ mod tests { provider_api_format: "provider_oauth:exchange".into(), model_name: Some("oauth-exchange".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2006,8 +2056,12 @@ mod tests { post(|Path(node_id): Path, body: Bytes| async move { let (meta, request_body) = decode_relay_envelope(&body); assert_eq!(node_id, "node-1"); + assert_eq!(meta["provider_id"], "prov-1"); + assert_eq!(meta["endpoint_id"], "ep-1"); + assert_eq!(meta["key_id"], "key-1"); assert_eq!(meta["http1_only"], true); assert_eq!(meta["follow_redirects"], json!(false)); + assert_eq!(meta["transport_profile"]["profile_id"], "relay-profile"); let request_json: serde_json::Value = serde_json::from_slice(&request_body).expect("request body should be json"); assert_eq!(request_json["model"], "gpt-4.1"); @@ -2047,7 +2101,9 @@ mod tests { provider_api_format: "provider_ops:verify".into(), model_name: Some("verify-auth".into()), proxy: Some(tunnel_proxy_snapshot(format!("http://{addr}"))), - tls_profile: None, + transport_profile: Some(ResolvedTransportProfile::from_legacy_tls_profile( + "relay-profile", + )), timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2107,7 +2163,7 @@ mod tests { provider_api_format: "claude:messages".into(), model_name: Some("claude-3.7-sonnet".into()), proxy: None, - tls_profile: Some("claude_code_nodejs".into()), + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2126,6 +2182,33 @@ mod tests { ); } + #[test] + fn direct_sync_execution_runtime_rejects_unsupported_transport_backend() { + let profile = ResolvedTransportProfile { + profile_id: "chrome-120".into(), + backend: "utls".into(), + http_mode: "auto".into(), + pool_scope: "key".into(), + extra: None, + }; + + let error = match build_client( + None, + None, + Some(&profile), + ExecutionTransportControls::default(), + ) { + Ok(_) => panic!("unsupported backend should fail"), + Err(error) => error, + }; + + assert!(matches!( + error, + ExecutionRuntimeTransportError::UnsupportedTransportProfile(backend) + if backend == "utls" + )); + } + #[tokio::test] async fn direct_sync_execution_runtime_compresses_json_body_when_requested() { let listener = crate::test_support::bind_loopback_listener() @@ -2182,7 +2265,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), @@ -2243,7 +2326,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-4.1".into()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(5_000), total_ms: Some(5_000), diff --git a/apps/aether-gateway/src/executor/candidate_loop.rs b/apps/aether-gateway/src/executor/candidate_loop.rs index aebfc7ff5..69c72151c 100644 --- a/apps/aether-gateway/src/executor/candidate_loop.rs +++ b/apps/aether-gateway/src/executor/candidate_loop.rs @@ -582,7 +582,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-test".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts, } } diff --git a/apps/aether-gateway/src/handlers/admin/features/gemini_files/upload/stage.rs b/apps/aether-gateway/src/handlers/admin/features/gemini_files/upload/stage.rs index 0c205f135..e841adccc 100644 --- a/apps/aether-gateway/src/handlers/admin/features/gemini_files/upload/stage.rs +++ b/apps/aether-gateway/src/handlers/admin/features/gemini_files/upload/stage.rs @@ -210,7 +210,7 @@ async fn admin_gemini_files_upload_single_key( proxy: state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await, - tls_profile: state.resolve_transport_tls_profile(&transport), + transport_profile: state.resolve_transport_profile(&transport), timeouts: state.resolve_transport_execution_timeouts(&transport), }; diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/antigravity.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/antigravity.rs index 1f92d97c8..f105bb62d 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/antigravity.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/antigravity.rs @@ -72,7 +72,7 @@ async fn execute_antigravity_quota_plan( provider_api_format: "antigravity:fetch_available_models".to_string(), model_name: Some("fetchAvailableModels".to_string()), proxy, - tls_profile: state.resolve_transport_tls_profile(transport), + transport_profile: state.resolve_transport_profile(transport), timeouts, }; diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/plan.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/plan.rs index da72d4535..1d9a4a27b 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/plan.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/codex/plan.rs @@ -97,7 +97,7 @@ pub(super) async fn execute_codex_quota_plan( provider_api_format: "openai:responses".to_string(), model_name: Some("codex-wham-usage".to_string()), proxy, - tls_profile: state.resolve_transport_tls_profile(transport), + transport_profile: state.resolve_transport_profile(transport), timeouts, }; execute_provider_quota_plan(state, transport, plan, "codex").await diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/kiro/plan.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/kiro/plan.rs index 5bac03c04..d6f4ae8b2 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/kiro/plan.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/kiro/plan.rs @@ -103,7 +103,7 @@ pub(super) async fn execute_kiro_quota_plan( provider_api_format: "kiro:usage".to_string(), model_name: Some("kiro-usage-limits".to_string()), proxy, - tls_profile: state.resolve_transport_tls_profile(transport), + transport_profile: state.resolve_transport_profile(transport), timeouts, }; diff --git a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/shared.rs b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/shared.rs index 1e8b99f59..d6a13bba9 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/shared.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/oauth/quota/shared.rs @@ -194,7 +194,6 @@ pub(super) async fn execute_provider_quota_plan( key_id = %transport.key.id, endpoint_id = %transport.endpoint.id, url = %plan.url, - tls_profile = ?plan.tls_profile.as_deref(), proxy_source = ?proxy_source, proxy_node_id = ?proxy_node_id, proxy_url_present, diff --git a/apps/aether-gateway/src/handlers/admin/provider/ops/providers/verify/request.rs b/apps/aether-gateway/src/handlers/admin/provider/ops/providers/verify/request.rs index bbc5e6b5b..4a96d0486 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/ops/providers/verify/request.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/ops/providers/verify/request.rs @@ -179,7 +179,7 @@ async fn admin_provider_ops_execute_request( provider_api_format: "provider_ops:verify".to_string(), model_name: Some("verify-auth".to_string()), proxy: proxy_snapshot.cloned(), - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS), read_ms: Some(ADMIN_PROVIDER_OPS_VERIFY_TIMEOUT_MS), diff --git a/apps/aether-gateway/src/handlers/admin/provider/query/models.rs b/apps/aether-gateway/src/handlers/admin/provider/query/models.rs index 1154ba03e..a0d1818d7 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/query/models.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/query/models.rs @@ -849,7 +849,7 @@ async fn provider_query_execute_kiro_test_candidate( proxy: state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await, - tls_profile: state.resolve_transport_tls_profile(&transport), + transport_profile: state.resolve_transport_profile(&transport), timeouts: state.resolve_transport_execution_timeouts(&transport), }; @@ -1214,7 +1214,7 @@ async fn provider_query_execute_standard_test_candidate( proxy: state .resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport) .await, - tls_profile: state.resolve_transport_tls_profile(&transport), + transport_profile: state.resolve_transport_profile(&transport), timeouts: state.resolve_transport_execution_timeouts(&transport), }; diff --git a/apps/aether-gateway/src/handlers/admin/provider/shared/payloads.rs b/apps/aether-gateway/src/handlers/admin/provider/shared/payloads.rs index bd523ea93..33d2ba929 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/shared/payloads.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/shared/payloads.rs @@ -44,6 +44,8 @@ pub(crate) struct AdminProviderKeyCreateRequest { pub(crate) model_include_patterns: Option>, #[serde(default)] pub(crate) model_exclude_patterns: Option>, + #[serde(default)] + pub(crate) fingerprint: Option, } #[derive(Debug, Deserialize)] diff --git a/apps/aether-gateway/src/handlers/admin/provider/write/keys/create.rs b/apps/aether-gateway/src/handlers/admin/provider/write/keys/create.rs index 2b3e7ae22..4d847edf2 100644 --- a/apps/aether-gateway/src/handlers/admin/provider/write/keys/create.rs +++ b/apps/aether-gateway/src/handlers/admin/provider/write/keys/create.rs @@ -170,7 +170,7 @@ pub(crate) async fn build_admin_create_provider_key_record( normalize_string_list(payload.allowed_models).map(|value| json!(value)), None, None, - None, + normalize_json_object(payload.fingerprint, "fingerprint")?, ) .map_err(|err| err.to_string())?; key.note = payload diff --git a/apps/aether-gateway/src/handlers/admin/request/provider/transport.rs b/apps/aether-gateway/src/handlers/admin/request/provider/transport.rs index eda2b6bd9..4901b0c81 100644 --- a/apps/aether-gateway/src/handlers/admin/request/provider/transport.rs +++ b/apps/aether-gateway/src/handlers/admin/request/provider/transport.rs @@ -221,6 +221,13 @@ impl<'a> AdminAppState<'a> { crate::provider_transport::resolve_transport_tls_profile(transport) } + pub(crate) fn resolve_transport_profile( + &self, + transport: &AdminGatewayProviderTransportSnapshot, + ) -> Option { + crate::provider_transport::resolve_transport_profile(transport) + } + pub(crate) fn resolve_transport_execution_timeouts( &self, transport: &AdminGatewayProviderTransportSnapshot, diff --git a/apps/aether-gateway/src/handlers/admin/system/proxy_nodes.rs b/apps/aether-gateway/src/handlers/admin/system/proxy_nodes.rs index cacc1eedc..d7a772a12 100644 --- a/apps/aether-gateway/src/handlers/admin/system/proxy_nodes.rs +++ b/apps/aether-gateway/src/handlers/admin/system/proxy_nodes.rs @@ -1261,12 +1261,16 @@ fn build_tunnel_probe_relay_envelope( timeout_secs: u64, ) -> Result, String> { let meta = crate::tunnel::tunnel_protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: probe_url.trim().to_string(), headers: std::collections::HashMap::new(), timeout: timeout_secs, follow_redirects: Some(false), http1_only: false, + transport_profile: None, }; let meta_bytes = serde_json::to_vec(&meta) .map_err(|error| format!("encode tunnel probe metadata failed: {error}"))?; diff --git a/apps/aether-gateway/src/handlers/public/support/test_connection/route.rs b/apps/aether-gateway/src/handlers/public/support/test_connection/route.rs index cb69e92f9..868006537 100644 --- a/apps/aether-gateway/src/handlers/public/support/test_connection/route.rs +++ b/apps/aether-gateway/src/handlers/public/support/test_connection/route.rs @@ -163,7 +163,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response( if transport.provider.proxy.is_some() || transport.endpoint.proxy.is_some() || transport.key.proxy.is_some() - || crate::provider_transport::resolve_transport_tls_profile(&transport).is_some() + || crate::provider_transport::resolve_transport_profile(&transport).is_some() { return None; } diff --git a/apps/aether-gateway/src/oauth/http_executor.rs b/apps/aether-gateway/src/oauth/http_executor.rs index d9124f737..803592901 100644 --- a/apps/aether-gateway/src/oauth/http_executor.rs +++ b/apps/aether-gateway/src/oauth/http_executor.rs @@ -69,7 +69,7 @@ impl<'a> OAuthHttpExecutor for GatewayOAuthHttpExecutor<'a> { provider_api_format: "oauth:exchange".to_string(), model_name: Some("oauth-exchange".to_string()), proxy: request.network.proxy, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(timeouts.connect_ms), read_ms: Some(timeouts.read_ms), diff --git a/apps/aether-gateway/src/orchestration/effects.rs b/apps/aether-gateway/src/orchestration/effects.rs index df7d52f8b..40a76c6ce 100644 --- a/apps/aether-gateway/src/orchestration/effects.rs +++ b/apps/aether-gateway/src/orchestration/effects.rs @@ -770,7 +770,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } @@ -794,7 +794,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/apps/aether-gateway/src/request_candidate_runtime.rs b/apps/aether-gateway/src/request_candidate_runtime.rs index 08da55c0f..381682018 100644 --- a/apps/aether-gateway/src/request_candidate_runtime.rs +++ b/apps/aether-gateway/src/request_candidate_runtime.rs @@ -753,7 +753,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/apps/aether-gateway/src/state/oauth.rs b/apps/aether-gateway/src/state/oauth.rs index a335e712e..d0f094b08 100644 --- a/apps/aether-gateway/src/state/oauth.rs +++ b/apps/aether-gateway/src/state/oauth.rs @@ -1313,7 +1313,7 @@ impl AppState { provider_api_format: "provider_oauth:local_refresh".to_string(), model_name: Some(provider_type.to_string()), proxy: proxy_snapshot, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(LOCAL_OAUTH_HTTP_TIMEOUT_MS), read_ms: Some(LOCAL_OAUTH_HTTP_TIMEOUT_MS), diff --git a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs index 0b99db4ba..a06b017d2 100644 --- a/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs +++ b/apps/aether-gateway/src/tests/ai_execute/finalize_local_cli/direct.rs @@ -568,7 +568,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz profile_arn: String, debug_tag: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -900,8 +900,8 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -1089,7 +1089,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz "proxy-node-kiro-cli-finalize-local" ); assert_eq!( - seen_remote_execution_runtime_request.tls_profile, + seen_remote_execution_runtime_request.transport_profile_id, "chrome_136" ); diff --git a/apps/aether-gateway/src/tests/ai_execute/stream/decision.rs b/apps/aether-gateway/src/tests/ai_execute/stream/decision.rs index d59301fec..d21009aec 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream/decision.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream/decision.rs @@ -943,7 +943,7 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision metadata_source: String, temperature_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -1258,8 +1258,8 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -1391,7 +1391,10 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision seen_execution_runtime_request.proxy_node_id, "proxy-node-openai-custom-stream" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-openai-chat-custom-stream-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/stream_cli/compact.rs b/apps/aether-gateway/src/tests/ai_execute/stream_cli/compact.rs index 17f0b8cb2..6bfb3f0c7 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream_cli/compact.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream_cli/compact.rs @@ -43,7 +43,7 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat instructions: String, store_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -398,8 +398,8 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -534,7 +534,10 @@ async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gat seen_execution_runtime_request.proxy_node_id, "proxy-node-openai-compact-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-openai-compact-local-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/stream_provider.rs b/apps/aether-gateway/src/tests/ai_execute/stream_provider.rs index 2af45c273..d9ba80e71 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream_provider.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream_provider.rs @@ -37,7 +37,7 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand current_content: String, debug_tag: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn crc32(data: &[u8]) -> u32 { @@ -404,8 +404,8 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -549,7 +549,10 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand seen_execution_runtime_request.proxy_node_id, "proxy-node-kiro-cli-local-stream" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-kiro-cli-local-stream-123") @@ -588,7 +591,7 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait metadata_mode: String, metadata_source: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -866,8 +869,8 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -992,7 +995,10 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait seen_execution_runtime_request.proxy_node_id, "proxy-node-claude-cli-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-claude-cli-local-stream-123") @@ -1037,7 +1043,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo metadata_source: String, assistant_content: serde_json::Value, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -1365,8 +1371,8 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -1517,7 +1523,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo "proxy-node-claude-code-cli-local" ); assert_eq!( - seen_execution_runtime_request.tls_profile, + seen_execution_runtime_request.transport_profile_id, "claude_code_nodejs" ); @@ -1557,7 +1563,7 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_ metadata_mode: String, metadata_source: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -1838,8 +1844,8 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_ .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -1950,7 +1956,10 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_ seen_execution_runtime_request.proxy_node_id, "proxy-node-claude-chat-stream" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-claude-chat-local-stream-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_chat.rs b/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_chat.rs index 724333a56..8e2f5caf8 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_chat.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_chat.rs @@ -25,7 +25,7 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_ metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -331,8 +331,8 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_ .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -441,7 +441,10 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_ seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-chat-stream" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-chat-local-stream-1") diff --git a/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs b/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs index 077c88b49..8a6414e5d 100644 --- a/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs +++ b/apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs @@ -25,7 +25,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -308,8 +308,8 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -418,7 +418,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-cli-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-cli-local-stream-123") @@ -457,7 +460,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_ metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } #[derive(Debug, Clone)] @@ -782,8 +785,8 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_ .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -927,7 +930,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_ seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-cli-oauth-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-cli-oauth-local-stream-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs b/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs index 8d2b0c21d..aef94f242 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/chat/local_decision.rs @@ -2605,7 +2605,7 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g metadata_source: String, temperature_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -2890,8 +2890,9 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -3018,7 +3019,10 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g seen_execution_runtime_request.proxy_node_id, "proxy-node-openai-custom-path" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); tokio::time::sleep(std::time::Duration::from_millis(100)).await; assert!( diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/claude/claude_code.rs b/apps/aether-gateway/src/tests/ai_execute/sync/claude/claude_code.rs index 2a3796c71..dd33a40d9 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/claude/claude_code.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/claude/claude_code.rs @@ -61,7 +61,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca metadata_source: String, assistant_content: serde_json::Value, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -393,8 +393,9 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -559,7 +560,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca "proxy-node-claude-code-cli-local" ); assert_eq!( - seen_execution_runtime_request.tls_profile, + seen_execution_runtime_request.transport_profile_id, "claude_code_nodejs" ); diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/claude/kiro.rs b/apps/aether-gateway/src/tests/ai_execute/sync/claude/kiro.rs index b49f417aa..bced648dd 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/claude/kiro.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/claude/kiro.rs @@ -57,7 +57,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid profile_arn: String, debug_tag: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn crc32(data: &[u8]) -> u32 { @@ -430,8 +430,8 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile").and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -579,7 +579,10 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid seen_execution_runtime_request.proxy_node_id, "proxy-node-kiro-cli-local-sync" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-kiro-cli-local-sync-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_chat.rs b/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_chat.rs index 2e7ee5099..36ee03c4e 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_chat.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_chat.rs @@ -56,7 +56,7 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy metadata_mode: String, metadata_source: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -335,8 +335,9 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -459,7 +460,10 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy seen_execution_runtime_request.proxy_node_id, "proxy-node-claude-chat-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-claude-chat-local-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_cli.rs b/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_cli.rs index 8bba0a0ef..07a26807e 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_cli.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/claude/local_cli.rs @@ -56,7 +56,7 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn metadata_mode: String, metadata_source: String, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -335,8 +335,9 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -467,7 +468,10 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn seen_execution_runtime_request.proxy_node_id, "proxy-node-claude-cli-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-claude-cli-local-sync-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/cli.rs b/apps/aether-gateway/src/tests/ai_execute/sync/cli.rs index 5b75bbc2c..4a5308e52 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/cli.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/cli.rs @@ -43,7 +43,7 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc metadata_origin: String, store_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -364,8 +364,9 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -499,7 +500,10 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc seen_execution_runtime_request.proxy_node_id, "proxy-node-openai-cli-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-openai-cli-local-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs b/apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs index 823953bd9..29183afde 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs @@ -55,7 +55,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -333,8 +333,9 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -452,7 +453,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-cli-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-cli-local-sync-123") @@ -775,7 +779,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } #[derive(Debug, Clone)] @@ -1095,8 +1099,9 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -1240,7 +1245,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-cli-oauth-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-cli-oauth-local-sync-123") diff --git a/apps/aether-gateway/src/tests/ai_execute/sync/gemini/local_chat.rs b/apps/aether-gateway/src/tests/ai_execute/sync/gemini/local_chat.rs index 67f95e2d1..8d309ade4 100644 --- a/apps/aether-gateway/src/tests/ai_execute/sync/gemini/local_chat.rs +++ b/apps/aether-gateway/src/tests/ai_execute/sync/gemini/local_chat.rs @@ -55,7 +55,7 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy metadata_source: String, tool_config_present: bool, proxy_node_id: String, - tls_profile: String, + transport_profile_id: String, } fn hash_api_key(value: &str) -> String { @@ -333,8 +333,9 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), - tls_profile: payload - .get("tls_profile") + transport_profile_id: payload + .get("transport_profile") + .and_then(|value| value.get("profile_id")) .and_then(|value| value.as_str()) .unwrap_or_default() .to_string(), @@ -442,7 +443,10 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy seen_execution_runtime_request.proxy_node_id, "proxy-node-gemini-chat-local" ); - assert_eq!(seen_execution_runtime_request.tls_profile, "chrome_136"); + assert_eq!( + seen_execution_runtime_request.transport_profile_id, + "chrome_136" + ); let stored_candidates = request_candidate_repository .list_by_request_id("trace-gemini-chat-local-123") diff --git a/apps/aether-gateway/src/tunnel/embedded/hub.rs b/apps/aether-gateway/src/tunnel/embedded/hub.rs index cea1a05b4..18fdbf5e1 100644 --- a/apps/aether-gateway/src/tunnel/embedded/hub.rs +++ b/apps/aether-gateway/src/tunnel/embedded/hub.rs @@ -1224,12 +1224,16 @@ mod tests { fn build_meta() -> protocol::RequestMeta { protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: "https://example.com".to_string(), headers: HashMap::new(), timeout: 30, follow_redirects: None, http1_only: false, + transport_profile: None, } } diff --git a/apps/aether-gateway/src/tunnel/embedded/local_relay.rs b/apps/aether-gateway/src/tunnel/embedded/local_relay.rs index f581f1ed2..82b7808b0 100644 --- a/apps/aether-gateway/src/tunnel/embedded/local_relay.rs +++ b/apps/aether-gateway/src/tunnel/embedded/local_relay.rs @@ -632,12 +632,16 @@ mod tests { ))); let meta = protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: "https://example.com/health".to_string(), headers: HashMap::new(), timeout: 30, follow_redirects: None, http1_only: false, + transport_profile: None, }; let request = Request::builder() .body(Body::from(encode_relay_envelope(&meta, &[]))) @@ -742,12 +746,16 @@ mod tests { ))); let meta = protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: "https://example.com/headers".to_string(), headers: HashMap::new(), timeout: 30, follow_redirects: None, http1_only: false, + transport_profile: None, }; let request = Request::builder() .body(Body::from(encode_relay_envelope(&meta, &[]))) diff --git a/apps/aether-gateway/src/tunnel/mod.rs b/apps/aether-gateway/src/tunnel/mod.rs index 85dc95579..e897b4ce2 100644 --- a/apps/aether-gateway/src/tunnel/mod.rs +++ b/apps/aether-gateway/src/tunnel/mod.rs @@ -481,12 +481,16 @@ impl EmbeddedTunnelState { ) -> Result { let timeout_secs = timeout_secs.clamp(5, 60); let meta = tunnel_protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: url.trim().to_string(), headers: HashMap::new(), timeout: timeout_secs, follow_redirects: Some(false), http1_only: false, + transport_profile: None, }; let stream = self.inner.hub.open_local_stream(node_id, &meta)?; let stream_id = stream.id; diff --git a/apps/aether-gateway/src/video_tasks/tests/fixtures.rs b/apps/aether-gateway/src/video_tasks/tests/fixtures.rs index a4f28082c..471e24ca8 100644 --- a/apps/aether-gateway/src/video_tasks/tests/fixtures.rs +++ b/apps/aether-gateway/src/video_tasks/tests/fixtures.rs @@ -98,7 +98,7 @@ pub(super) fn sample_plan(url: &str, provider_api_format: &str) -> ExecutionPlan url: None, extra: None, }), - tls_profile: Some("chrome".to_string()), + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(10_000), read_ms: Some(30_000), diff --git a/apps/aether-proxy/src/app.rs b/apps/aether-proxy/src/app.rs index 56ae8ab27..6f472fba2 100644 --- a/apps/aether-proxy/src/app.rs +++ b/apps/aether-proxy/src/app.rs @@ -134,12 +134,11 @@ pub async fn run(mut config: Config, servers: Vec) -> anyhow::Resul config.dns_cache_capacity, )); - // Build Hyper client for tunnel upstream requests (shared). - // DNS still flows through validated addresses from DnsCache, while the - // custom connector exposes per-request connect/TLS timing when available. - let upstream_client = upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache)); - let upstream_http1_client = - upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache)); + let config = Arc::new(config); + + // Build a profile-keyed Hyper client pool for tunnel upstream requests. + let upstream_client_pool = + upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache)); // Register with each Aether server and build per-server contexts. // Wrapped in Arc so retry_failed_registrations can append later. @@ -196,10 +195,9 @@ pub async fn run(mut config: Config, servers: Vec) -> anyhow::Resul // Build shared application state let tunnel_tls_config = Arc::new(crate::tunnel::client::build_tls_config()); let mut state = AppState { - config: Arc::new(config), + config, dns_cache, - upstream_client, - upstream_http1_client, + upstream_client_pool, tunnel_tls_config, stream_gate: None, distributed_stream_gate: None, @@ -916,15 +914,12 @@ mod tests { fn sample_state(config: Config) -> Arc { let config = Arc::new(config); let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128)); - let upstream_client = - upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache)); - let upstream_http1_client = - upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache)); + let upstream_client_pool = + upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache)); Arc::new(ProxyAppState { config, dns_cache, - upstream_client, - upstream_http1_client, + upstream_client_pool, tunnel_tls_config: Arc::new(crate::tunnel::client::build_tls_config()), stream_gate: None, distributed_stream_gate: None, diff --git a/apps/aether-proxy/src/state.rs b/apps/aether-proxy/src/state.rs index 6156cd7b7..b455fcff9 100644 --- a/apps/aether-proxy/src/state.rs +++ b/apps/aether-proxy/src/state.rs @@ -13,17 +13,15 @@ use crate::config::Config; use crate::registration::client::AetherClient; use crate::runtime::SharedDynamicConfig; use crate::target_filter::DnsCache; -use crate::upstream_client::UpstreamClient; +use crate::upstream_client::UpstreamClientPool; /// Central application state shared across all servers/tunnels. pub struct AppState { pub config: Arc, /// DNS cache for upstream target resolution (shared). pub dns_cache: Arc, - /// Hyper client for tunnel upstream requests with validated DNS and connection timing. - pub upstream_client: UpstreamClient, - /// Dedicated Hyper client that forces HTTP/1.1 for upstreams that break on H2/ALPN. - pub upstream_http1_client: UpstreamClient, + /// Profile-keyed upstream client pool used by tunnel requests. + pub upstream_client_pool: UpstreamClientPool, /// Shared TLS config for tunnel WebSocket connections (avoids re-parsing root CAs on each reconnect). pub tunnel_tls_config: Arc, /// Optional per-process stream admission gate. diff --git a/apps/aether-proxy/src/tunnel/mod.rs b/apps/aether-proxy/src/tunnel/mod.rs index a67fb22c9..46ab3b3c3 100644 --- a/apps/aether-proxy/src/tunnel/mod.rs +++ b/apps/aether-proxy/src/tunnel/mod.rs @@ -379,12 +379,16 @@ mod tests { fn relay_probe_envelope() -> Vec { let meta = protocol::RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: "http://127.0.0.1:80/blocked".to_string(), headers: std::collections::HashMap::new(), timeout: 5, follow_redirects: None, http1_only: false, + transport_profile: None, }; let meta_json = serde_json::to_vec(&meta).expect("tunnel relay probe metadata should serialize"); @@ -446,15 +450,12 @@ mod tests { fn sample_state(config: Config) -> Arc { let config = Arc::new(config); let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128)); - let upstream_client = - upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache)); - let upstream_http1_client = - upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache)); + let upstream_client_pool = + upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache)); Arc::new(ProxyAppState { config, dns_cache, - upstream_client, - upstream_http1_client, + upstream_client_pool, tunnel_tls_config: Arc::new(crate::tunnel::client::build_tls_config()), stream_gate: None, distributed_stream_gate: None, diff --git a/apps/aether-proxy/src/tunnel/stream_handler.rs b/apps/aether-proxy/src/tunnel/stream_handler.rs index 9d7fdba1d..93e5700b6 100644 --- a/apps/aether-proxy/src/tunnel/stream_handler.rs +++ b/apps/aether-proxy/src/tunnel/stream_handler.rs @@ -726,6 +726,7 @@ fn resolve_redirect( async fn execute_upstream_request( state: &AppState, server: &ServerContext, + meta: &RequestMeta, current_url: &url::Url, method: hyper::Method, headers: &[(String, String)], @@ -756,11 +757,14 @@ async fn execute_upstream_request( } let dns_ms = dns_start.elapsed().as_millis() as u64; - let client = if http1_only { - &state.upstream_http1_client - } else { - &state.upstream_client - }; + let client_key = upstream_client::upstream_client_pool_key( + meta.provider_id.as_deref(), + meta.endpoint_id.as_deref(), + meta.key_id.as_deref(), + meta.transport_profile.as_ref(), + http1_only, + ); + let client = state.upstream_client_pool.get_or_build(client_key)?; let mut request = hyper::Request::builder() .method(method) @@ -1026,15 +1030,16 @@ async fn relay_upstream_response( } #[cfg(test)] -fn upstream_client_for_request<'a>( - state: &'a AppState, +fn upstream_client_pool_key_for_request( meta: &RequestMeta, -) -> &'a upstream_client::UpstreamClient { - if meta.http1_only { - &state.upstream_http1_client - } else { - &state.upstream_client - } +) -> upstream_client::UpstreamClientPoolKey { + upstream_client::upstream_client_pool_key( + meta.provider_id.as_deref(), + meta.endpoint_id.as_deref(), + meta.key_id.as_deref(), + meta.transport_profile.as_ref(), + meta.http1_only, + ) } /// Handle a single stream: receive body, execute upstream, send response. @@ -1235,6 +1240,7 @@ async fn handle_stream_inner( let response_ctx = match execute_upstream_request( state, server, + &meta, ¤t_url, current_method.clone(), ¤t_headers, @@ -1671,19 +1677,40 @@ mod tests { #[test] fn selects_http1_only_client_when_request_metadata_requires_it() { - let state = sample_state(None, None); let default_meta = sample_request_meta(); - assert!(std::ptr::eq( - upstream_client_for_request(state.as_ref(), &default_meta), - &state.upstream_client - )); + assert_eq!( + upstream_client_pool_key_for_request(&default_meta).http_mode, + "auto" + ); let mut http1_meta = sample_request_meta(); http1_meta.http1_only = true; - assert!(std::ptr::eq( - upstream_client_for_request(state.as_ref(), &http1_meta), - &state.upstream_http1_client - )); + assert_eq!( + upstream_client_pool_key_for_request(&http1_meta).http_mode, + "http1_only" + ); + } + + #[test] + fn upstream_client_pool_key_isolates_accounts() { + let mut first = sample_request_meta(); + first.provider_id = Some("provider-1".to_string()); + first.endpoint_id = Some("endpoint-1".to_string()); + first.key_id = Some("key-1".to_string()); + first.transport_profile = Some(aether_contracts::ResolvedTransportProfile { + profile_id: "profile-a".to_string(), + backend: "reqwest_rustls".to_string(), + http_mode: "auto".to_string(), + pool_scope: "key".to_string(), + extra: None, + }); + let mut second = first.clone(); + second.key_id = Some("key-2".to_string()); + + assert_ne!( + upstream_client_pool_key_for_request(&first), + upstream_client_pool_key_for_request(&second) + ); } #[test] @@ -2210,12 +2237,16 @@ mod tests { fn sample_request_meta() -> RequestMeta { RequestMeta { + provider_id: None, + endpoint_id: None, + key_id: None, method: "GET".to_string(), url: "https://example.com/ok".to_string(), headers: HashMap::new(), timeout: 30, follow_redirects: None, http1_only: false, + transport_profile: None, } } @@ -2226,15 +2257,12 @@ mod tests { ensure_rustls_provider(); let config = Arc::new(sample_config()); let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128)); - let upstream_client = - upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache)); - let upstream_http1_client = - upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache)); + let upstream_client_pool = + upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache)); Arc::new(AppState { config, dns_cache, - upstream_client, - upstream_http1_client, + upstream_client_pool, tunnel_tls_config: Arc::new(build_tls_config()), stream_gate, distributed_stream_gate, @@ -2259,15 +2287,12 @@ mod tests { fn sample_state_with_config(config: Config) -> Arc { let config = Arc::new(config); let dns_cache = Arc::new(DnsCache::new(Duration::from_secs(60), 128)); - let upstream_client = - upstream_client::build_upstream_client(&config, Arc::clone(&dns_cache)); - let upstream_http1_client = - upstream_client::build_http1_only_upstream_client(&config, Arc::clone(&dns_cache)); + let upstream_client_pool = + upstream_client::UpstreamClientPool::new(Arc::clone(&config), Arc::clone(&dns_cache)); Arc::new(AppState { config, dns_cache, - upstream_client, - upstream_http1_client, + upstream_client_pool, tunnel_tls_config: Arc::new(build_tls_config()), stream_gate: None, distributed_stream_gate: None, diff --git a/apps/aether-proxy/src/upstream_client.rs b/apps/aether-proxy/src/upstream_client.rs index a432df4b9..1c546ccc3 100644 --- a/apps/aether-proxy/src/upstream_client.rs +++ b/apps/aether-proxy/src/upstream_client.rs @@ -1,12 +1,18 @@ +use std::collections::HashMap; use std::convert::Infallible; use std::future::Future; use std::io; use std::net::IpAddr; use std::pin::Pin; use std::sync::Arc; +use std::sync::Mutex; use std::task::{Context, Poll}; use std::time::Duration; +use aether_contracts::{ + ResolvedTransportProfile, TRANSPORT_BACKEND_HYPER_RUSTLS, TRANSPORT_BACKEND_REQWEST_RUSTLS, + TRANSPORT_HTTP_MODE_HTTP1_ONLY, +}; use bytes::Bytes; use futures_util::Stream; use http_body_util::combinators::UnsyncBoxBody; @@ -37,6 +43,115 @@ type TlsStream = TokioIo>; pub type UpstreamRequestBody = UnsyncBoxBody; pub type UpstreamClient = Client; +const DEFAULT_PROFILE_ID: &str = "default"; +const DEFAULT_BACKEND: &str = TRANSPORT_BACKEND_HYPER_RUSTLS; +const DEFAULT_HTTP_MODE: &str = "auto"; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct UpstreamClientPoolKey { + pub provider_id: String, + pub endpoint_id: String, + pub key_id: String, + pub profile_id: String, + pub backend: String, + pub http_mode: String, +} + +#[derive(Clone)] +pub struct UpstreamClientPool { + config: Arc, + dns_cache: Arc, + clients: Arc>>, +} + +impl UpstreamClientPool { + pub fn new(config: Arc, dns_cache: Arc) -> Self { + Self { + config, + dns_cache, + clients: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub fn get_or_build(&self, key: UpstreamClientPoolKey) -> Result { + if let Some(client) = self + .clients + .lock() + .expect("client pool lock") + .get(&key) + .cloned() + { + return Ok(client); + } + + validate_proxy_transport_backend(&key.backend)?; + let http1_only = key + .http_mode + .eq_ignore_ascii_case(TRANSPORT_HTTP_MODE_HTTP1_ONLY); + let client = build_upstream_client_with_protocol( + &self.config, + Arc::clone(&self.dns_cache), + http1_only, + ); + self.clients + .lock() + .expect("client pool lock") + .insert(key, client.clone()); + Ok(client) + } +} + +pub fn upstream_client_pool_key( + provider_id: Option<&str>, + endpoint_id: Option<&str>, + key_id: Option<&str>, + profile: Option<&ResolvedTransportProfile>, + http1_only: bool, +) -> UpstreamClientPoolKey { + let profile_http_mode = profile + .map(|profile| profile.http_mode.trim()) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_HTTP_MODE); + let http_mode = if http1_only { + TRANSPORT_HTTP_MODE_HTTP1_ONLY + } else { + profile_http_mode + }; + UpstreamClientPoolKey { + provider_id: normalized_pool_key_part(provider_id), + endpoint_id: normalized_pool_key_part(endpoint_id), + key_id: normalized_pool_key_part(key_id), + profile_id: profile + .map(|profile| profile.profile_id.trim()) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_PROFILE_ID) + .to_string(), + backend: profile + .map(|profile| profile.backend.trim()) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_BACKEND) + .to_string(), + http_mode: http_mode.to_string(), + } +} + +fn normalized_pool_key_part(value: Option<&str>) -> String { + value + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or("-") + .to_string() +} + +fn validate_proxy_transport_backend(backend: &str) -> Result<(), String> { + if backend.eq_ignore_ascii_case(TRANSPORT_BACKEND_HYPER_RUSTLS) + || backend.eq_ignore_ascii_case(TRANSPORT_BACKEND_REQWEST_RUSTLS) + { + return Ok(()); + } + Err(format!("unsupported transport profile backend: {backend}")) +} + pub fn stream_request_body(stream: S) -> UpstreamRequestBody where S: Stream, io::Error>> + Send + 'static, @@ -181,17 +296,6 @@ impl Service for InstrumentedConnector { } } -pub fn build_upstream_client(config: &Config, dns_cache: Arc) -> UpstreamClient { - build_upstream_client_with_protocol(config, dns_cache, false) -} - -pub fn build_http1_only_upstream_client( - config: &Config, - dns_cache: Arc, -) -> UpstreamClient { - build_upstream_client_with_protocol(config, dns_cache, true) -} - fn build_upstream_client_with_protocol( config: &Config, dns_cache: Arc, @@ -424,6 +528,7 @@ impl rt::Write for MaybeHttpsStream { #[cfg(test)] mod tests { use super::*; + use aether_contracts::ResolvedTransportProfile; use hyper::Response; #[test] @@ -476,4 +581,36 @@ mod tests { assert_eq!(timing.response_wait_ms, 320); assert!(!timing.connection_reused); } + + #[test] + fn upstream_client_pool_key_includes_profile_identity() { + let profile = ResolvedTransportProfile { + profile_id: "profile-a".to_string(), + backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(), + http_mode: "auto".to_string(), + pool_scope: "key".to_string(), + extra: None, + }; + let pool_key = upstream_client_pool_key( + Some("provider-1"), + Some("endpoint-1"), + Some("key-1"), + Some(&profile), + false, + ); + + assert_eq!(pool_key.provider_id, "provider-1"); + assert_eq!(pool_key.endpoint_id, "endpoint-1"); + assert_eq!(pool_key.key_id, "key-1"); + assert_eq!(pool_key.profile_id, "profile-a"); + assert_eq!(pool_key.backend, TRANSPORT_BACKEND_REQWEST_RUSTLS); + assert_eq!(pool_key.http_mode, "auto"); + } + + #[test] + fn upstream_client_pool_rejects_unsupported_backend() { + let error = validate_proxy_transport_backend("utls").unwrap_err(); + + assert!(error.contains("unsupported transport profile backend")); + } } diff --git a/crates/aether-ai-serving/src/attempt_plan.rs b/crates/aether-ai-serving/src/attempt_plan.rs index ced24ae83..8336b878c 100644 --- a/crates/aether-ai-serving/src/attempt_plan.rs +++ b/crates/aether-ai-serving/src/attempt_plan.rs @@ -130,7 +130,7 @@ pub fn build_ai_execution_plan_from_decision( provider_api_format: parts.core.provider_api_format, model_name: payload.model_name.take(), proxy: payload.proxy.take(), - tls_profile: payload.tls_profile.take(), + transport_profile: payload.transport_profile.take(), timeouts: payload.timeouts.take(), } } @@ -156,7 +156,7 @@ pub fn build_ai_execution_decision_from_plan( provider_api_format, model_name, proxy, - tls_profile, + transport_profile, timeouts, } = parts.plan; let auth_pair = parts @@ -209,7 +209,7 @@ pub fn build_ai_execution_decision_from_plan( provider_request_body_base64: body_bytes_b64, content_type, proxy, - tls_profile, + transport_profile, timeouts, upstream_is_stream: stream, report_kind: parts.report_kind, @@ -451,7 +451,7 @@ mod tests { provider_api_format: "claude:messages".to_string(), model_name: Some("mapped".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -512,7 +512,7 @@ mod tests { provider_request_body_base64: None, content_type: None, proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: None, diff --git a/crates/aether-ai-serving/src/decision_payload.rs b/crates/aether-ai-serving/src/decision_payload.rs index fb406eb44..f4b82be1d 100644 --- a/crates/aether-ai-serving/src/decision_payload.rs +++ b/crates/aether-ai-serving/src/decision_payload.rs @@ -4,7 +4,7 @@ use aether_ai_formats::api::{ ExecutionRuntimeAuthContext, EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION, }; -use aether_contracts::{ExecutionTimeouts, ProxySnapshot}; +use aether_contracts::{ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile}; use crate::{AiExecutionDecision, ConversionMode, ExecutionStrategy}; @@ -34,7 +34,7 @@ pub struct AiExecutionDecisionResponseParts { pub provider_request_body_base64: Option, pub content_type: Option, pub proxy: Option, - pub tls_profile: Option, + pub transport_profile: Option, pub timeouts: Option, pub upstream_is_stream: bool, pub report_kind: Option, @@ -74,7 +74,7 @@ pub fn build_ai_execution_decision_response( provider_request_body_base64: parts.provider_request_body_base64, content_type: parts.content_type, proxy: parts.proxy, - tls_profile: parts.tls_profile, + transport_profile: parts.transport_profile, timeouts: parts.timeouts, upstream_is_stream: parts.upstream_is_stream, report_kind: parts.report_kind, diff --git a/crates/aether-ai-serving/src/dto.rs b/crates/aether-ai-serving/src/dto.rs index e1df6a456..2cdac6160 100644 --- a/crates/aether-ai-serving/src/dto.rs +++ b/crates/aether-ai-serving/src/dto.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use aether_ai_formats::api::ExecutionRuntimeAuthContext; -use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot}; +use aether_contracts::{ExecutionPlan, ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -117,7 +117,7 @@ pub struct AiExecutionDecision { #[serde(default)] pub proxy: Option, #[serde(default)] - pub tls_profile: Option, + pub transport_profile: Option, #[serde(default)] pub timeouts: Option, #[serde(default)] @@ -217,7 +217,7 @@ mod tests { provider_request_body_base64: None, content_type: None, proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, upstream_is_stream: false, report_kind: None, diff --git a/crates/aether-ai-serving/src/plan_payload.rs b/crates/aether-ai-serving/src/plan_payload.rs index 3252847e8..8b56e2b60 100644 --- a/crates/aether-ai-serving/src/plan_payload.rs +++ b/crates/aether-ai-serving/src/plan_payload.rs @@ -101,7 +101,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("model".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/crates/aether-contracts/src/lib.rs b/crates/aether-contracts/src/lib.rs index ea5ce1311..195aae51f 100644 --- a/crates/aether-contracts/src/lib.rs +++ b/crates/aether-contracts/src/lib.rs @@ -8,8 +8,10 @@ mod usage; pub use error::{ExecutionError, ExecutionErrorKind, ExecutionPhase}; pub use frame::{StreamFrame, StreamFramePayload, StreamFrameType}; pub use plan::{ - ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody, + ExecutionPlan, ExecutionTimeouts, ProxySnapshot, RequestBody, ResolvedTransportProfile, EXECUTION_REQUEST_FOLLOW_REDIRECTS_HEADER, EXECUTION_REQUEST_HTTP1_ONLY_HEADER, + TRANSPORT_BACKEND_HYPER_RUSTLS, TRANSPORT_BACKEND_REQWEST_RUSTLS, TRANSPORT_HTTP_MODE_AUTO, + TRANSPORT_HTTP_MODE_HTTP1_ONLY, TRANSPORT_POOL_SCOPE_KEY, }; pub use result::{ExecutionResult, ExecutionTelemetry, ResponseBody}; pub use usage::{ExecutionStreamTerminalSummary, StandardizedUsage}; diff --git a/crates/aether-contracts/src/plan.rs b/crates/aether-contracts/src/plan.rs index fbde167ef..5fa0fd873 100644 --- a/crates/aether-contracts/src/plan.rs +++ b/crates/aether-contracts/src/plan.rs @@ -59,6 +59,47 @@ pub struct ProxySnapshot { pub extra: Option, } +pub const TRANSPORT_BACKEND_REQWEST_RUSTLS: &str = "reqwest_rustls"; +pub const TRANSPORT_BACKEND_HYPER_RUSTLS: &str = "hyper_rustls"; +pub const TRANSPORT_HTTP_MODE_AUTO: &str = "auto"; +pub const TRANSPORT_HTTP_MODE_HTTP1_ONLY: &str = "http1_only"; +pub const TRANSPORT_POOL_SCOPE_KEY: &str = "key"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(default)] +pub struct ResolvedTransportProfile { + pub profile_id: String, + pub backend: String, + pub http_mode: String, + pub pool_scope: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub extra: Option, +} + +impl Default for ResolvedTransportProfile { + fn default() -> Self { + Self { + profile_id: String::new(), + backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(), + http_mode: TRANSPORT_HTTP_MODE_AUTO.to_string(), + pool_scope: TRANSPORT_POOL_SCOPE_KEY.to_string(), + extra: None, + } + } +} + +impl ResolvedTransportProfile { + pub fn from_legacy_tls_profile(profile_id: impl Into) -> Self { + Self { + profile_id: profile_id.into(), + backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(), + http_mode: TRANSPORT_HTTP_MODE_AUTO.to_string(), + pool_scope: TRANSPORT_POOL_SCOPE_KEY.to_string(), + extra: None, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ExecutionPlan { pub request_id: String, @@ -88,7 +129,7 @@ pub struct ExecutionPlan { #[serde(default, skip_serializing_if = "Option::is_none")] pub proxy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub tls_profile: Option, + pub transport_profile: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub timeouts: Option, } @@ -117,7 +158,7 @@ mod tests { provider_api_format: "openai:chat".into(), model_name: Some("gpt-test".into()), proxy: None, - tls_profile: Some("chrome".into()), + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(30_000), read_ms: Some(3_600_000), diff --git a/crates/aether-contracts/src/tunnel.rs b/crates/aether-contracts/src/tunnel.rs index c1b1e5f4d..71798817f 100644 --- a/crates/aether-contracts/src/tunnel.rs +++ b/crates/aether-contracts/src/tunnel.rs @@ -171,6 +171,12 @@ pub enum ProtocolError { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RequestMeta { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub endpoint_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key_id: Option, pub method: String, pub url: String, pub headers: std::collections::HashMap, @@ -180,6 +186,8 @@ pub struct RequestMeta { pub follow_redirects: Option, #[serde(default, skip_serializing_if = "is_false")] pub http1_only: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transport_profile: Option, } fn default_timeout() -> u64 { diff --git a/crates/aether-model-fetch/src/strategy.rs b/crates/aether-model-fetch/src/strategy.rs index e6c38b75b..e9409af01 100644 --- a/crates/aether-model-fetch/src/strategy.rs +++ b/crates/aether-model-fetch/src/strategy.rs @@ -4,7 +4,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use aether_contracts::{ExecutionPlan, ExecutionResult, RequestBody}; use aether_provider_transport::{ is_vertex_api_key_transport_context, resolve_transport_execution_timeouts, - resolve_transport_tls_profile, GatewayProviderTransportSnapshot, + resolve_transport_profile, GatewayProviderTransportSnapshot, }; use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::Engine as _; @@ -499,6 +499,8 @@ async fn exchange_vertex_service_account_token( let body = format!( "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion={assertion}" ); + let transport_profile = resolve_transport_profile(transport); + let plan = ExecutionPlan { request_id: format!("req-model-fetch-{}-vertex-sa-token", transport.key.id), candidate_id: None, @@ -524,7 +526,7 @@ async fn exchange_vertex_service_account_token( provider_api_format: "vertex_ai:service_account_token".to_string(), model_name: Some("token".to_string()), proxy: runtime.resolve_model_fetch_proxy(transport).await, - tls_profile: resolve_transport_tls_profile(transport), + transport_profile, timeouts: resolve_transport_execution_timeouts(transport), }; let result = runtime.execute_model_fetch_execution_plan(&plan).await?; diff --git a/crates/aether-model-fetch/src/transport.rs b/crates/aether-model-fetch/src/transport.rs index 105fcaeef..65c8acd71 100644 --- a/crates/aether-model-fetch/src/transport.rs +++ b/crates/aether-model-fetch/src/transport.rs @@ -11,7 +11,7 @@ use aether_provider_transport::auth::{ }; use aether_provider_transport::vertex::resolve_local_vertex_api_key_query_auth; use aether_provider_transport::{ - apply_local_header_rules, resolve_transport_execution_timeouts, resolve_transport_tls_profile, + apply_local_header_rules, resolve_transport_execution_timeouts, resolve_transport_profile, GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth, }; use async_trait::async_trait; @@ -295,6 +295,8 @@ async fn build_execution_plan( model_name, } = request; + let transport_profile = resolve_transport_profile(transport); + Ok(ExecutionPlan { request_id: format!( "req-model-fetch-{}-{}", @@ -317,7 +319,7 @@ async fn build_execution_plan( provider_api_format, model_name, proxy: runtime.resolve_model_fetch_proxy(transport).await, - tls_profile: resolve_transport_tls_profile(transport), + transport_profile, timeouts: resolve_transport_execution_timeouts(transport), }) } diff --git a/crates/aether-provider-transport/src/claude_code/policy.rs b/crates/aether-provider-transport/src/claude_code/policy.rs index f4caf676c..4f56150bf 100644 --- a/crates/aether-provider-transport/src/claude_code/policy.rs +++ b/crates/aether-provider-transport/src/claude_code/policy.rs @@ -1,7 +1,7 @@ use super::super::snapshot::GatewayProviderTransportSnapshot; use super::super::{ body_rules_are_locally_supported, header_rules_are_locally_supported, - resolve_transport_tls_profile, supports_local_oauth_request_auth_resolution, + resolve_transport_profile, supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported, }; use super::auth::supports_local_claude_code_auth; @@ -52,7 +52,7 @@ pub fn local_claude_code_transport_unsupported_reason_with_network( if !transport_proxy_is_locally_supported(transport) { return Some("transport_proxy_unsupported"); } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return Some("transport_tls_profile_unsupported"); } diff --git a/crates/aether-provider-transport/src/diagnostics.rs b/crates/aether-provider-transport/src/diagnostics.rs index 22cb514dc..a0d4e5e31 100644 --- a/crates/aether-provider-transport/src/diagnostics.rs +++ b/crates/aether-provider-transport/src/diagnostics.rs @@ -8,7 +8,9 @@ use crate::conversion::{ request_conversion_enabled_for_transport, request_conversion_transport_unsupported_reason, request_pair_allowed_for_transport, }; -use crate::network::{resolve_transport_tls_profile, transport_proxy_is_locally_supported}; +use crate::network::{ + resolve_transport_profile, resolve_transport_tls_profile, transport_proxy_is_locally_supported, +}; use crate::policy::{ local_gemini_transport_unsupported_reason_with_network, local_openai_chat_transport_unsupported_reason, @@ -67,6 +69,9 @@ pub fn build_transport_diagnostics( provider_api_format: &str, ) -> Value { let resolved_tls_profile = resolve_transport_tls_profile(transport); + let resolved_transport_profile = resolve_transport_profile(transport) + .and_then(|profile| serde_json::to_value(profile).ok()) + .unwrap_or(Value::Null); let configured_tls_profile = transport .key .fingerprint @@ -75,6 +80,23 @@ pub fn build_transport_diagnostics( .and_then(|value| value.get("tls_profile")) .cloned() .unwrap_or(Value::Null); + let configured_key_transport_profile = transport + .key + .fingerprint + .as_ref() + .and_then(Value::as_object) + .and_then(|value| value.get("transport_profile")) + .cloned() + .unwrap_or(Value::Null); + let configured_provider_transport_profile = transport + .provider + .config + .as_ref() + .and_then(|value| value.get("fingerprint")) + .and_then(Value::as_object) + .and_then(|value| value.get("transport_profile")) + .cloned() + .unwrap_or(Value::Null); let has_oauth_config = transport.key.decrypted_auth_config.is_some(); let oauth_resolution_supported = !has_oauth_config || crate::supports_local_oauth_request_auth_resolution(transport); @@ -111,7 +133,10 @@ pub fn build_transport_diagnostics( }, "fingerprint": transport.key.fingerprint, "configured_tls_profile": configured_tls_profile, + "configured_key_transport_profile": configured_key_transport_profile, + "configured_provider_transport_profile": configured_provider_transport_profile, "resolved_tls_profile": resolved_tls_profile, + "resolved_transport_profile": resolved_transport_profile, "request_pair": { "client_api_format": client_api_format, "provider_api_format": provider_api_format, diff --git a/crates/aether-provider-transport/src/kiro/policy.rs b/crates/aether-provider-transport/src/kiro/policy.rs index 58bbd5d27..e6b823643 100644 --- a/crates/aether-provider-transport/src/kiro/policy.rs +++ b/crates/aether-provider-transport/src/kiro/policy.rs @@ -1,5 +1,5 @@ use super::super::snapshot::GatewayProviderTransportSnapshot; -use super::super::{resolve_transport_tls_profile, transport_proxy_is_locally_supported}; +use super::super::{resolve_transport_profile, transport_proxy_is_locally_supported}; use super::{ body_rules_are_locally_supported, header_rules_are_locally_supported, supports_local_kiro_request_auth_resolution, supports_local_kiro_request_shape, PROVIDER_TYPE, @@ -48,7 +48,7 @@ pub fn local_kiro_request_transport_unsupported_reason_with_network( if !transport_proxy_is_locally_supported(transport) { return Some("transport_proxy_unsupported"); } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return Some("transport_tls_profile_unsupported"); } diff --git a/crates/aether-provider-transport/src/lib.rs b/crates/aether-provider-transport/src/lib.rs index 7adb5aa6f..ee2622769 100644 --- a/crates/aether-provider-transport/src/lib.rs +++ b/crates/aether-provider-transport/src/lib.rs @@ -46,10 +46,10 @@ pub use generic_oauth::{ }; pub use headers::{should_skip_request_header, should_skip_upstream_passthrough_header}; pub use network::{ - resolve_transport_execution_timeouts, resolve_transport_proxy_snapshot, - resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile, - transport_proxy_is_locally_supported, TransportTunnelAffinityLookup, - TransportTunnelAttachmentOwner, + resolve_transport_execution_timeouts, resolve_transport_profile, + resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity, + resolve_transport_tls_profile, transport_proxy_is_locally_supported, + TransportTunnelAffinityLookup, TransportTunnelAttachmentOwner, }; pub use oauth_refresh::{ supports_local_oauth_request_auth_resolution, CachedOAuthEntry, LocalOAuthHttpExecutor, diff --git a/crates/aether-provider-transport/src/network.rs b/crates/aether-provider-transport/src/network.rs index b701b38ed..93426b9b1 100644 --- a/crates/aether-provider-transport/src/network.rs +++ b/crates/aether-provider-transport/src/network.rs @@ -1,4 +1,7 @@ -use aether_contracts::{ExecutionTimeouts, ProxySnapshot}; +use aether_contracts::{ + ExecutionTimeouts, ProxySnapshot, ResolvedTransportProfile, TRANSPORT_BACKEND_REQWEST_RUSTLS, + TRANSPORT_HTTP_MODE_AUTO, TRANSPORT_POOL_SCOPE_KEY, +}; use async_trait::async_trait; use serde_json::{json, Map, Value}; use tracing::warn; @@ -141,15 +144,83 @@ pub fn transport_proxy_is_locally_supported(transport: &GatewayProviderTransport pub fn resolve_transport_tls_profile( transport: &GatewayProviderTransportSnapshot, ) -> Option { - transport - .key - .fingerprint - .as_ref() - .and_then(|value| value.get("tls_profile")) + resolve_transport_profile(transport).map(|profile| profile.profile_id) +} + +pub fn resolve_transport_profile( + transport: &GatewayProviderTransportSnapshot, +) -> Option { + resolve_transport_profile_from_fingerprint(transport.key.fingerprint.as_ref()).or_else(|| { + resolve_transport_profile_from_provider_config(transport.provider.config.as_ref()) + }) +} + +fn resolve_transport_profile_from_provider_config( + config: Option<&Value>, +) -> Option { + let fingerprint = config?.get("fingerprint"); + resolve_transport_profile_from_fingerprint(fingerprint) +} + +fn resolve_transport_profile_from_fingerprint( + fingerprint: Option<&Value>, +) -> Option { + let fingerprint = fingerprint?; + if let Some(profile) = fingerprint.get("transport_profile") { + if let Some(resolved) = parse_transport_profile_value(profile) { + return Some(resolved); + } + } + + fingerprint + .get("tls_profile") .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) + .map(ResolvedTransportProfile::from_legacy_tls_profile) +} + +fn parse_transport_profile_value(value: &Value) -> Option { + if let Some(profile_id) = value + .as_str() + .map(str::trim) + .filter(|value| !value.is_empty()) + { + return Some(ResolvedTransportProfile { + profile_id: profile_id.to_string(), + backend: TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string(), + http_mode: TRANSPORT_HTTP_MODE_AUTO.to_string(), + pool_scope: TRANSPORT_POOL_SCOPE_KEY.to_string(), + extra: None, + }); + } + + let object = value.as_object()?; + let profile_id = json_string_field(object, "profile_id") + .or_else(|| json_string_field(object, "id")) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty())?; + let backend = json_string_field(object, "backend") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| TRANSPORT_BACKEND_REQWEST_RUSTLS.to_string()); + let http_mode = json_string_field(object, "http_mode") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| TRANSPORT_HTTP_MODE_AUTO.to_string()); + let pool_scope = json_string_field(object, "pool_scope") + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| TRANSPORT_POOL_SCOPE_KEY.to_string()); + let extra = object.get("extra").cloned(); + + Some(ResolvedTransportProfile { + profile_id, + backend, + http_mode, + pool_scope, + extra, + }) } fn effective_proxy_config(transport: &GatewayProviderTransportSnapshot) -> Option<&Value> { @@ -225,9 +296,10 @@ mod tests { GatewayProviderTransportProvider, GatewayProviderTransportSnapshot, }; use super::{ - resolve_transport_proxy_snapshot, resolve_transport_proxy_snapshot_with_tunnel_affinity, - resolve_transport_tls_profile, transport_proxy_is_locally_supported, - TransportTunnelAffinityLookup, TransportTunnelAttachmentOwner, + resolve_transport_profile, resolve_transport_proxy_snapshot, + resolve_transport_proxy_snapshot_with_tunnel_affinity, resolve_transport_tls_profile, + transport_proxy_is_locally_supported, TransportTunnelAffinityLookup, + TransportTunnelAttachmentOwner, }; #[derive(Default)] @@ -391,4 +463,60 @@ mod tests { ); assert!(transport_proxy_is_locally_supported(&sample_transport())); } + + #[test] + fn resolves_transport_profile_from_key_fingerprint_before_provider_default() { + let mut transport = sample_transport(); + transport.provider.config = Some(json!({ + "fingerprint": {"transport_profile": "provider_profile"} + })); + transport.key.fingerprint = Some(json!({ + "transport_profile": { + "profile_id": "key_profile", + "backend": "reqwest_rustls", + "http_mode": "http1_only" + } + })); + + let profile = resolve_transport_profile(&transport).expect("profile"); + + assert_eq!(profile.profile_id, "key_profile"); + assert_eq!(profile.backend, "reqwest_rustls"); + assert_eq!(profile.http_mode, "http1_only"); + assert_eq!(profile.pool_scope, "key"); + } + + #[test] + fn resolves_transport_profile_from_provider_default() { + let mut transport = sample_transport(); + transport.key.fingerprint = None; + transport.provider.config = Some(json!({ + "fingerprint": {"transport_profile": "provider_profile"} + })); + + let profile = resolve_transport_profile(&transport).expect("profile"); + + assert_eq!(profile.profile_id, "provider_profile"); + assert_eq!(profile.backend, "reqwest_rustls"); + } + + #[test] + fn maps_legacy_tls_profile_to_transport_profile() { + let profile = resolve_transport_profile(&sample_transport()).expect("profile"); + + assert_eq!(profile.profile_id, "chrome_136"); + assert_eq!(profile.backend, "reqwest_rustls"); + assert_eq!(profile.http_mode, "auto"); + assert_eq!(profile.pool_scope, "key"); + } + + #[test] + fn resolves_no_transport_profile_without_fingerprint_configuration() { + let mut transport = sample_transport(); + transport.key.fingerprint = None; + transport.provider.config = None; + + assert!(resolve_transport_profile(&transport).is_none()); + assert!(resolve_transport_tls_profile(&transport).is_none()); + } } diff --git a/crates/aether-provider-transport/src/policy.rs b/crates/aether-provider-transport/src/policy.rs index e47001263..83edb7f8a 100644 --- a/crates/aether-provider-transport/src/policy.rs +++ b/crates/aether-provider-transport/src/policy.rs @@ -6,7 +6,7 @@ use super::provider_types::{ use super::snapshot::GatewayProviderTransportSnapshot; use super::{ body_rules_are_locally_supported, header_rules_are_locally_supported, - resolve_transport_tls_profile, supports_local_oauth_request_auth_resolution, + resolve_transport_profile, supports_local_oauth_request_auth_resolution, transport_proxy_is_locally_supported, }; @@ -76,7 +76,7 @@ pub fn local_openai_chat_transport_unsupported_reason( if !transport_proxy_is_locally_supported(transport) { return Some("transport_proxy_unsupported"); } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return Some("transport_tls_profile_unsupported"); } if !provider_type_supports_local_openai_chat_transport(&transport.provider.provider_type) { @@ -175,8 +175,7 @@ fn local_same_format_transport_unsupported_reason( if !transport_proxy_is_locally_supported(transport) { return Some("transport_proxy_unsupported"); } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() - { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return Some("transport_tls_profile_unsupported"); } } else if transport.provider.proxy.is_some() diff --git a/crates/aether-provider-transport/src/vertex/policy.rs b/crates/aether-provider-transport/src/vertex/policy.rs index 97557c226..4e550fdd3 100644 --- a/crates/aether-provider-transport/src/vertex/policy.rs +++ b/crates/aether-provider-transport/src/vertex/policy.rs @@ -1,7 +1,7 @@ use super::super::snapshot::GatewayProviderTransportSnapshot; use super::super::{ body_rules_are_locally_supported, header_rules_are_locally_supported, - resolve_transport_tls_profile, transport_proxy_is_locally_supported, + resolve_transport_profile, resolve_transport_tls_profile, transport_proxy_is_locally_supported, }; use super::auth::resolve_local_vertex_api_key_query_auth; @@ -46,7 +46,7 @@ pub fn local_vertex_api_key_gemini_transport_unsupported_reason_with_network( if !transport_proxy_is_locally_supported(transport) { return Some("transport_proxy_unsupported"); } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return Some("transport_tls_profile_unsupported"); } @@ -133,8 +133,7 @@ fn supports_local_vertex_api_key_same_format_transport( if !transport_proxy_is_locally_supported(transport) { return false; } - if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() - { + if transport.key.fingerprint.is_some() && resolve_transport_profile(transport).is_none() { return false; } } else if transport.provider.proxy.is_some() diff --git a/crates/aether-scheduler-core/src/request_candidate.rs b/crates/aether-scheduler-core/src/request_candidate.rs index 75adfc729..ede53013e 100644 --- a/crates/aether-scheduler-core/src/request_candidate.rs +++ b/crates/aether-scheduler-core/src/request_candidate.rs @@ -818,7 +818,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/crates/aether-testkit/src/bin/capacity_curve_baseline.rs b/crates/aether-testkit/src/bin/capacity_curve_baseline.rs index ec5fa57fb..364f8d2a2 100644 --- a/crates/aether-testkit/src/bin/capacity_curve_baseline.rs +++ b/crates/aether-testkit/src/bin/capacity_curve_baseline.rs @@ -536,7 +536,7 @@ fn execution_plan(url: String, stream: bool) -> ExecutionPlan { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(2_000), read_ms: Some(10_000), @@ -608,6 +608,10 @@ fn relay_envelope() -> Vec { timeout: 30, follow_redirects: None, http1_only: false, + provider_id: None, + endpoint_id: None, + key_id: None, + transport_profile: None, }; let meta_json = serde_json::to_vec(&meta).expect("hub relay metadata should serialize"); let body = br#"{"model":"gpt-5","messages":[{"role":"user","content":"hello"}]}"#; diff --git a/crates/aether-testkit/src/bin/gateway_tunnel_stream_baseline.rs b/crates/aether-testkit/src/bin/gateway_tunnel_stream_baseline.rs index 45127c7b7..b390f59bb 100644 --- a/crates/aether-testkit/src/bin/gateway_tunnel_stream_baseline.rs +++ b/crates/aether-testkit/src/bin/gateway_tunnel_stream_baseline.rs @@ -100,6 +100,10 @@ fn relay_envelope() -> Vec { timeout: 30, follow_redirects: None, http1_only: false, + provider_id: None, + endpoint_id: None, + key_id: None, + transport_profile: None, }; let meta_json = serde_json::to_vec(&meta).expect("tunnel relay metadata should serialize"); let body = br#"{"model":"gpt-5","messages":[{"role":"user","content":"hello"}]}"#; diff --git a/crates/aether-testkit/src/bin/multi_instance_admission_baseline.rs b/crates/aether-testkit/src/bin/multi_instance_admission_baseline.rs index 58a2e93be..c1f57ccb6 100644 --- a/crates/aether-testkit/src/bin/multi_instance_admission_baseline.rs +++ b/crates/aether-testkit/src/bin/multi_instance_admission_baseline.rs @@ -386,7 +386,7 @@ fn execution_plan(url: String) -> ExecutionPlan { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(2_000), read_ms: Some(10_000), diff --git a/crates/aether-testkit/src/bin/multi_instance_owner_relay_baseline.rs b/crates/aether-testkit/src/bin/multi_instance_owner_relay_baseline.rs index 0b35b059f..57ed3db04 100644 --- a/crates/aether-testkit/src/bin/multi_instance_owner_relay_baseline.rs +++ b/crates/aether-testkit/src/bin/multi_instance_owner_relay_baseline.rs @@ -256,6 +256,10 @@ fn relay_envelope() -> Vec { timeout: 30, follow_redirects: None, http1_only: false, + provider_id: None, + endpoint_id: None, + key_id: None, + transport_profile: None, }; let meta_json = serde_json::to_vec(&meta).expect("owner relay metadata should serialize"); let body = br#"{"model":"gpt-5","messages":[{"role":"user","content":"owner relay"}]}"#; diff --git a/crates/aether-testkit/src/bin/single_instance_baseline.rs b/crates/aether-testkit/src/bin/single_instance_baseline.rs index 71668208f..9e7fd1f59 100644 --- a/crates/aether-testkit/src/bin/single_instance_baseline.rs +++ b/crates/aether-testkit/src/bin/single_instance_baseline.rs @@ -257,7 +257,7 @@ fn execution_plan(url: String, stream: bool) -> ExecutionPlan { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: Some(ExecutionTimeouts { connect_ms: Some(2_000), read_ms: Some(10_000), diff --git a/crates/aether-usage-runtime/src/request_metadata.rs b/crates/aether-usage-runtime/src/request_metadata.rs index 4b7ec9af2..13c00a711 100644 --- a/crates/aether-usage-runtime/src/request_metadata.rs +++ b/crates/aether-usage-runtime/src/request_metadata.rs @@ -376,7 +376,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, } } diff --git a/crates/aether-usage-runtime/src/write.rs b/crates/aether-usage-runtime/src/write.rs index e55de6361..e64e36159 100644 --- a/crates/aether-usage-runtime/src/write.rs +++ b/crates/aether-usage-runtime/src/write.rs @@ -2483,7 +2483,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -2550,7 +2550,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -2595,7 +2595,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; @@ -2652,7 +2652,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -2715,7 +2715,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewayStreamReportRequest { @@ -2810,7 +2810,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let mut standardized_usage = StandardizedUsage::new(); @@ -2881,7 +2881,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let mut partial_summary_usage = StandardizedUsage::new(); @@ -2987,7 +2987,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let sse_body = concat!( @@ -3111,7 +3111,7 @@ mod tests { provider_api_format: "gemini:generate_content".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3218,7 +3218,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3326,7 +3326,7 @@ mod tests { provider_api_format: "gemini:generate_content".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3398,7 +3398,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3461,7 +3461,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewayStreamReportRequest { @@ -3547,7 +3547,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewayStreamReportRequest { @@ -3613,7 +3613,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3667,7 +3667,7 @@ mod tests { provider_api_format: "openai:responses".to_string(), model_name: Some("gpt-5.4".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; let payload = GatewaySyncReportRequest { @@ -3915,7 +3915,7 @@ mod tests { provider_api_format: "openai:chat".to_string(), model_name: Some("gpt-5".to_string()), proxy: None, - tls_profile: None, + transport_profile: None, timeouts: None, }; diff --git a/crates/aether-video-tasks-core/src/gemini.rs b/crates/aether-video-tasks-core/src/gemini.rs index a31a9a356..427fc3211 100644 --- a/crates/aether-video-tasks-core/src/gemini.rs +++ b/crates/aether-video-tasks-core/src/gemini.rs @@ -168,7 +168,7 @@ impl GeminiVideoTaskSeed { provider_api_format: "gemini:video".to_string(), model_name: Some(self.model.clone()), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }) } @@ -228,7 +228,7 @@ impl GeminiVideoTaskSeed { provider_api_format: "gemini:video".to_string(), model_name: Some(self.model.clone()), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }, report_kind: Some("gemini_video_cancel_sync_finalize".to_string()), diff --git a/crates/aether-video-tasks-core/src/openai.rs b/crates/aether-video-tasks-core/src/openai.rs index b762928cf..ec164d1d3 100644 --- a/crates/aether-video-tasks-core/src/openai.rs +++ b/crates/aether-video-tasks-core/src/openai.rs @@ -235,7 +235,7 @@ impl OpenAiVideoTaskSeed { .clone() .or_else(|| self.transport.model_name.clone()), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }, ))) @@ -340,7 +340,7 @@ impl OpenAiVideoTaskSeed { provider_api_format: "openai:video".to_string(), model_name: model_name.clone(), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }, report_kind: Some("openai_video_delete_sync_finalize".to_string()), @@ -405,7 +405,7 @@ impl OpenAiVideoTaskSeed { .clone() .or_else(|| self.transport.model_name.clone()), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }) } @@ -466,7 +466,7 @@ impl OpenAiVideoTaskSeed { provider_api_format: "openai:video".to_string(), model_name: model_name.clone(), proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }, report_kind: Some("openai_video_cancel_sync_finalize".to_string()), @@ -561,7 +561,7 @@ impl OpenAiVideoTaskSeed { provider_api_format: "openai:video".to_string(), model_name, proxy: self.transport.proxy.clone(), - tls_profile: self.transport.tls_profile.clone(), + transport_profile: None, timeouts: self.transport.timeouts.clone(), }, report_kind: Some("openai_video_remix_sync_finalize".to_string()), diff --git a/crates/aether-video-tasks-core/src/sync.rs b/crates/aether-video-tasks-core/src/sync.rs index 853d0e2da..2bf49bd57 100644 --- a/crates/aether-video-tasks-core/src/sync.rs +++ b/crates/aether-video-tasks-core/src/sync.rs @@ -278,7 +278,7 @@ pub fn build_internal_finalize_video_plan( url: None, extra: None, }), - tls_profile: None, + transport_profile: None, timeouts: None, }) } diff --git a/crates/aether-video-tasks-core/src/transport_domain.rs b/crates/aether-video-tasks-core/src/transport_domain.rs index 29b107272..44bfcefed 100644 --- a/crates/aether-video-tasks-core/src/transport_domain.rs +++ b/crates/aether-video-tasks-core/src/transport_domain.rs @@ -30,7 +30,7 @@ impl LocalVideoTaskTransport { content_type: plan.content_type.clone(), model_name: plan.model_name.clone(), proxy: plan.proxy.clone(), - tls_profile: plan.tls_profile.clone(), + tls_profile: None, timeouts: plan.timeouts.clone(), }) }