mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: 请求候选追踪添加 proxy 元数据, 修复 usage 状态回退, 优化前端轮询
- 在各 planner decision payload 中注入 proxy trace 信息 (node_id, node_name, url, source) - request_candidate 报告上下文支持 proxy 字段, extra_data 合并逻辑改为 merge 而非覆盖 - SQL/内存仓库防止 usage status 从 streaming 回退到 pending - 前端移除活跃请求完成时的全表刷新, active discovery 尊重 globalAutoRefresh 开关
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use aether_contracts::ProxySnapshot;
|
||||
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@@ -24,6 +25,29 @@ pub(crate) struct LocalExecutionCandidateMetadataParts<'a> {
|
||||
pub(crate) extra_fields: Map<String, Value>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_request_trace_proxy_value(
|
||||
transport: Option<&GatewayProviderTransportSnapshot>,
|
||||
resolved_proxy: Option<&ProxySnapshot>,
|
||||
) -> Option<Value> {
|
||||
let resolved_proxy = resolved_proxy?;
|
||||
let mut object = Map::new();
|
||||
|
||||
if let Some(node_id) = trimmed_non_empty(resolved_proxy.node_id.as_deref()) {
|
||||
object.insert("node_id".to_string(), Value::String(node_id));
|
||||
}
|
||||
if let Some(node_name) = trimmed_non_empty(resolved_proxy.label.as_deref()) {
|
||||
object.insert("node_name".to_string(), Value::String(node_name));
|
||||
}
|
||||
if let Some(url) = sanitize_trace_proxy_url(resolved_proxy.url.as_deref()) {
|
||||
object.insert("url".to_string(), Value::String(url));
|
||||
}
|
||||
if let Some(source) = resolve_request_trace_proxy_source(transport, true) {
|
||||
object.insert("source".to_string(), Value::String(source.to_string()));
|
||||
}
|
||||
|
||||
(!object.is_empty()).then_some(Value::Object(object))
|
||||
}
|
||||
|
||||
pub(crate) fn build_local_execution_candidate_metadata(
|
||||
parts: LocalExecutionCandidateMetadataParts<'_>,
|
||||
) -> Value {
|
||||
@@ -250,6 +274,67 @@ fn summarize_proxy_config(proxy: Option<&Value>) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_request_trace_proxy_source(
|
||||
transport: Option<&GatewayProviderTransportSnapshot>,
|
||||
has_resolved_proxy: bool,
|
||||
) -> Option<&'static str> {
|
||||
let transport = transport?;
|
||||
if transport_has_explicit_proxy(transport.key.proxy.as_ref()) {
|
||||
return Some("key");
|
||||
}
|
||||
if transport_has_explicit_proxy(transport.endpoint.proxy.as_ref()) {
|
||||
return Some("endpoint");
|
||||
}
|
||||
if transport_has_explicit_proxy(transport.provider.proxy.as_ref()) {
|
||||
return Some("provider");
|
||||
}
|
||||
has_resolved_proxy.then_some("system")
|
||||
}
|
||||
|
||||
fn transport_has_explicit_proxy(proxy: Option<&Value>) -> bool {
|
||||
let Some(object) = proxy.and_then(Value::as_object) else {
|
||||
return false;
|
||||
};
|
||||
let enabled = object
|
||||
.get("enabled")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
if !enabled {
|
||||
return false;
|
||||
}
|
||||
|
||||
object
|
||||
.get("node_id")
|
||||
.or_else(|| object.get("url"))
|
||||
.or_else(|| object.get("proxy_url"))
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn sanitize_trace_proxy_url(url: Option<&str>) -> Option<String> {
|
||||
let raw = url.map(str::trim).filter(|value| !value.is_empty())?;
|
||||
let parsed = url::Url::parse(raw).ok()?;
|
||||
let scheme = parsed.scheme().trim();
|
||||
let host = parsed.host_str()?.trim();
|
||||
if scheme.is_empty() || host.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut safe = format!("{scheme}://{host}");
|
||||
if let Some(port) = parsed.port() {
|
||||
safe.push(':');
|
||||
safe.push_str(port.to_string().as_str());
|
||||
}
|
||||
Some(safe)
|
||||
}
|
||||
|
||||
fn trimmed_non_empty(value: Option<&str>) -> Option<String> {
|
||||
value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn resolve_request_transport_unsupported_reason(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
client_api_format: &str,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::ai_pipeline::planner::candidate_materialization::mark_skipped_local_execution_candidate;
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::materialization_policy::{
|
||||
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
|
||||
};
|
||||
@@ -60,6 +61,11 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
|
||||
.await;
|
||||
let tls_profile = resolve_transport_tls_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())
|
||||
{
|
||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||
}
|
||||
if resolved.is_kiro {
|
||||
extra_fields.insert(
|
||||
"envelope_name".to_string(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::payload_metadata::{
|
||||
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
|
||||
};
|
||||
@@ -58,6 +59,9 @@ pub(super) async fn maybe_build_local_gemini_files_decision_payload_for_candidat
|
||||
.await;
|
||||
let tls_profile = resolve_transport_tls_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);
|
||||
}
|
||||
extra_fields.insert("file_key_id".to_string(), json!(candidate.key_id));
|
||||
extra_fields.insert("file_name".to_string(), json!(resolved.file_name));
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::payload_metadata::{
|
||||
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
|
||||
};
|
||||
@@ -42,6 +43,10 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
|
||||
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&transport)
|
||||
.await;
|
||||
let tls_profile = resolve_transport_tls_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);
|
||||
}
|
||||
|
||||
Some(build_local_execution_decision_response(
|
||||
LocalExecutionDecisionResponseParts {
|
||||
@@ -103,7 +108,7 @@ pub(super) async fn maybe_build_local_video_create_decision_payload_for_candidat
|
||||
original_request_body: body_json,
|
||||
has_envelope: false,
|
||||
needs_conversion: false,
|
||||
extra_fields: serde_json::Map::new(),
|
||||
extra_fields,
|
||||
},
|
||||
)),
|
||||
auth_context: input.auth_context.clone(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::ai_pipeline::planner::candidate_materialization::mark_skipped_local_execution_candidate;
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::materialization_policy::{
|
||||
build_local_candidate_persistence_policy, LocalCandidatePersistencePolicyKind,
|
||||
};
|
||||
@@ -41,6 +42,15 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
state, parts, trace_id, body_json, input, &attempt, spec,
|
||||
)
|
||||
.await?;
|
||||
let proxy = state
|
||||
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport)
|
||||
.await;
|
||||
let mut extra_fields = serde_json::Map::new();
|
||||
if let Some(proxy_value) =
|
||||
build_request_trace_proxy_value(Some(&resolved.transport), proxy.as_ref())
|
||||
{
|
||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||
}
|
||||
|
||||
Some(build_local_execution_decision_response(
|
||||
LocalExecutionDecisionResponseParts {
|
||||
@@ -68,9 +78,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
provider_request_body: Some(resolved.provider_request_body.clone()),
|
||||
provider_request_body_base64: None,
|
||||
content_type: Some("application/json".to_string()),
|
||||
proxy: state
|
||||
.resolve_transport_proxy_snapshot_with_tunnel_affinity(&resolved.transport)
|
||||
.await,
|
||||
proxy,
|
||||
tls_profile: resolve_transport_tls_profile(&resolved.transport),
|
||||
timeouts: resolve_transport_execution_timeouts(&resolved.transport),
|
||||
upstream_is_stream: resolved.upstream_is_stream,
|
||||
@@ -99,7 +107,7 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
|
||||
original_request_body: body_json,
|
||||
has_envelope: false,
|
||||
needs_conversion: true,
|
||||
extra_fields: serde_json::Map::new(),
|
||||
extra_fields,
|
||||
}),
|
||||
ExecutionStrategy::LocalCrossFormat,
|
||||
ConversionMode::Bidirectional,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::payload_metadata::{
|
||||
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
|
||||
};
|
||||
@@ -60,6 +61,12 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
||||
.await;
|
||||
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
|
||||
let timeouts = resolve_transport_execution_timeouts(&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())
|
||||
{
|
||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||
}
|
||||
|
||||
Some(build_local_execution_decision_response(
|
||||
LocalExecutionDecisionResponseParts {
|
||||
@@ -119,7 +126,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
|
||||
resolved.conversion_mode,
|
||||
crate::ai_pipeline::ConversionMode::Bidirectional
|
||||
),
|
||||
extra_fields: serde_json::Map::new(),
|
||||
extra_fields,
|
||||
}),
|
||||
resolved.execution_strategy,
|
||||
resolved.conversion_mode,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use serde_json::json;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::ai_pipeline::planner::candidate_metadata::build_request_trace_proxy_value;
|
||||
use crate::ai_pipeline::planner::payload_metadata::{
|
||||
build_local_execution_decision_response, LocalExecutionDecisionResponseParts,
|
||||
};
|
||||
@@ -62,6 +63,11 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
|
||||
let tls_profile = resolve_transport_tls_profile(&resolved.transport);
|
||||
let timeouts = resolve_transport_execution_timeouts(&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())
|
||||
{
|
||||
extra_fields.insert("proxy".to_string(), proxy_value);
|
||||
}
|
||||
if resolved.is_antigravity {
|
||||
extra_fields.insert("envelope_name".to_string(), json!("antigravity:v1internal"));
|
||||
}
|
||||
|
||||
@@ -501,6 +501,24 @@ async fn gateway_executes_openai_cli_sync_via_local_decision_gate_with_local_syn
|
||||
.expect("request candidate trace should read");
|
||||
assert_eq!(stored_candidates.len(), 1);
|
||||
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Success);
|
||||
assert_eq!(
|
||||
stored_candidates[0]
|
||||
.extra_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("proxy"))
|
||||
.and_then(|value| value.get("node_id"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("proxy-node-openai-cli-local")
|
||||
);
|
||||
assert_eq!(
|
||||
stored_candidates[0]
|
||||
.extra_data
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("proxy"))
|
||||
.and_then(|value| value.get("source"))
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("key")
|
||||
);
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user