mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
Implement transport profile routing
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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()),
|
||||
|
||||
+3
-3
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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<String, String>,
|
||||
@@ -124,6 +130,8 @@ struct RelayRequestMeta {
|
||||
follow_redirects: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
http1_only: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
transport_profile: Option<ResolvedTransportProfile>,
|
||||
}
|
||||
|
||||
#[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<reqwest::Client, ExecutionRuntimeTransportError> {
|
||||
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<String>, 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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ pub(crate) struct AdminProviderKeyCreateRequest {
|
||||
pub(crate) model_include_patterns: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub(crate) model_exclude_patterns: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub(crate) fingerprint: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<aether_contracts::ResolvedTransportProfile> {
|
||||
crate::provider_transport::resolve_transport_profile(transport)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_transport_execution_timeouts(
|
||||
&self,
|
||||
transport: &AdminGatewayProviderTransportSnapshot,
|
||||
|
||||
@@ -1261,12 +1261,16 @@ fn build_tunnel_probe_relay_envelope(
|
||||
timeout_secs: u64,
|
||||
) -> Result<Vec<u8>, 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}"))?;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, &[])))
|
||||
|
||||
@@ -481,12 +481,16 @@ impl EmbeddedTunnelState {
|
||||
) -> Result<TunnelProbeResponse, String> {
|
||||
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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -134,12 +134,11 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> 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<Mutex> so retry_failed_registrations can append later.
|
||||
@@ -196,10 +195,9 @@ pub async fn run(mut config: Config, servers: Vec<ServerEntry>) -> 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<ProxyAppState> {
|
||||
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,
|
||||
|
||||
@@ -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<Config>,
|
||||
/// DNS cache for upstream target resolution (shared).
|
||||
pub dns_cache: Arc<DnsCache>,
|
||||
/// 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<rustls::ClientConfig>,
|
||||
/// Optional per-process stream admission gate.
|
||||
|
||||
@@ -379,12 +379,16 @@ mod tests {
|
||||
|
||||
fn relay_probe_envelope() -> Vec<u8> {
|
||||
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<ProxyAppState> {
|
||||
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,
|
||||
|
||||
@@ -726,6 +726,7 @@ fn resolve_redirect<B>(
|
||||
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<AppState> {
|
||||
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,
|
||||
|
||||
@@ -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<tokio_rustls::client::TlsStream<TcpStream>>;
|
||||
pub type UpstreamRequestBody = UnsyncBoxBody<Bytes, io::Error>;
|
||||
pub type UpstreamClient = Client<InstrumentedConnector, UpstreamRequestBody>;
|
||||
|
||||
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<Config>,
|
||||
dns_cache: Arc<DnsCache>,
|
||||
clients: Arc<Mutex<HashMap<UpstreamClientPoolKey, UpstreamClient>>>,
|
||||
}
|
||||
|
||||
impl UpstreamClientPool {
|
||||
pub fn new(config: Arc<Config>, dns_cache: Arc<DnsCache>) -> Self {
|
||||
Self {
|
||||
config,
|
||||
dns_cache,
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_build(&self, key: UpstreamClientPoolKey) -> Result<UpstreamClient, String> {
|
||||
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<S>(stream: S) -> UpstreamRequestBody
|
||||
where
|
||||
S: Stream<Item = Result<Frame<Bytes>, io::Error>> + Send + 'static,
|
||||
@@ -181,17 +296,6 @@ impl Service<Uri> for InstrumentedConnector {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_upstream_client(config: &Config, dns_cache: Arc<DnsCache>) -> UpstreamClient {
|
||||
build_upstream_client_with_protocol(config, dns_cache, false)
|
||||
}
|
||||
|
||||
pub fn build_http1_only_upstream_client(
|
||||
config: &Config,
|
||||
dns_cache: Arc<DnsCache>,
|
||||
) -> UpstreamClient {
|
||||
build_upstream_client_with_protocol(config, dns_cache, true)
|
||||
}
|
||||
|
||||
fn build_upstream_client_with_protocol(
|
||||
config: &Config,
|
||||
dns_cache: Arc<DnsCache>,
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user