feat: 新增 frontdoor 执行回环守卫与多项可观测性增强

- 新增 frontdoor_loop_guard 模块,检测并拒绝 execution runtime 回环到本地网关的请求(HTTP 508)
- candidate loop 引入 span tracking、执行尝试日志与流式看门狗超时
- 本地故障转移策略支持从 report_context 加载,新增 append_local_failover_policy_to_value
- runtime tracing 美化:移除 identity 前缀,按 span 深度树形缩进,target 固定宽度展示
- Codex OpenAI CLI 补齐 chatgpt-account-id/x-client-request-id/session_id/conversation_id 请求头
- OpenAI CLI same/cross-format 聚合规则放宽以支持 openai:compact 客户端格式,并过滤 error-like 响应体
- auth/proxy/finalize 日志补充 user_id/api_key_id/api_key_name/balance_remaining 等字段
- 启动日志拆分为 starting/ready/config 三段,新增 resolve_bind_http_base_url
- access_log middleware 将生成的 trace_id 回注到下游请求头
- Cargo.toml 启用 serde_json preserve_order 特性
This commit is contained in:
fawney19
2026-04-11 01:50:24 +08:00
parent 3f057628b7
commit 6144473ebe
38 changed files with 1957 additions and 421 deletions
@@ -6,10 +6,10 @@ use crate::ai_pipeline::{
};
use crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL;
use crate::scheduler::config::{read_scheduler_ordering_config, SchedulerOrderingConfig};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use aether_scheduler_core::{
build_scheduler_affinity_cache_key_for_api_key_id, compare_candidates_by_priority_mode,
requested_capability_priority_for_candidate, SchedulerAffinityTarget,
SchedulerMinimalCandidateSelectionCandidate,
};
const PLANNER_SCHEDULER_AFFINITY_MAX_ENTRIES: usize = 10_000;
@@ -23,8 +23,9 @@ use crate::ai_pipeline::{
};
use crate::clock::current_unix_ms;
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
};
use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
@@ -263,45 +264,48 @@ pub(crate) async fn maybe_build_local_same_format_provider_decision_payload_for_
resolve_transport_proxy_snapshot_with_tunnel_affinity(planner_state.app(), &transport)
.await;
let tls_profile = resolve_transport_tls_profile(&transport);
let report_context = append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": spec.api_format,
"client_api_format": spec.api_format,
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": is_kiro || is_antigravity,
"envelope_name": if is_kiro {
Some(KIRO_ENVELOPE_NAME)
} else if is_antigravity {
Some(super::super::ANTIGRAVITY_ENVELOPE_NAME)
} else {
None
},
"needs_conversion": false,
}),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
spec.api_format,
spec.api_format,
let report_context = append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": spec.api_format,
"client_api_format": spec.api_format,
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": is_kiro || is_antigravity,
"envelope_name": if is_kiro {
Some(KIRO_ENVELOPE_NAME)
} else if is_antigravity {
Some(super::super::ANTIGRAVITY_ENVELOPE_NAME)
} else {
None
},
"needs_conversion": false,
}),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
spec.api_format,
spec.api_format,
),
&transport,
);
Some(GatewayControlSyncDecisionResponse {
@@ -16,8 +16,9 @@ use crate::ai_pipeline::{collect_control_headers, ConversionMode, ExecutionStrat
use crate::ai_pipeline::{LocalResolvedOAuthRequestAuth, PlannerAppState};
use crate::clock::current_unix_ms;
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
};
use super::{LocalStandardCandidateAttempt, LocalStandardDecisionInput, LocalStandardSpec};
@@ -334,38 +335,41 @@ pub(super) async fn maybe_build_local_standard_decision_payload_for_candidate(
timeouts: resolve_transport_execution_timeouts(&transport),
upstream_is_stream,
report_kind: Some(spec.report_kind.to_string()),
report_context: Some(append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": candidate.provider_name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": provider_api_format,
"client_api_format": spec.api_format,
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": true,
}),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
spec.api_format,
candidate.endpoint_api_format.as_str(),
report_context: Some(append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": candidate.provider_name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": provider_api_format,
"client_api_format": spec.api_format,
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": true,
}),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
spec.api_format,
candidate.endpoint_api_format.as_str(),
),
&transport,
)),
auth_context: Some(input.auth_context.clone()),
})
@@ -25,7 +25,8 @@ use crate::ai_pipeline::transport::{
use crate::ai_pipeline::{ConversionMode, ExecutionStrategy, PlannerAppState};
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth};
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse,
};
use super::support::{mark_skipped_local_openai_chat_candidate, LocalOpenAiChatDecisionInput};
@@ -287,38 +288,41 @@ pub(super) async fn build_cross_format_local_openai_chat_decision_payload_for_ca
timeouts: resolve_transport_execution_timeouts(transport),
upstream_is_stream,
report_kind: Some(report_kind.to_string()),
report_context: Some(append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": provider_api_format,
"client_api_format": "openai:chat",
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": true,
}),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
"openai:chat",
provider_api_format.as_str(),
report_context: Some(append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": provider_api_format,
"client_api_format": "openai:chat",
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": true,
}),
ExecutionStrategy::LocalCrossFormat,
ConversionMode::Bidirectional,
"openai:chat",
provider_api_format.as_str(),
),
transport,
)),
auth_context: Some(input.auth_context.clone()),
})
@@ -22,7 +22,8 @@ use crate::ai_pipeline::{
};
use crate::ai_pipeline::{GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth};
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse,
};
use super::support::{mark_skipped_local_openai_chat_candidate, LocalOpenAiChatDecisionInput};
@@ -230,38 +231,41 @@ pub(super) async fn build_same_format_local_openai_chat_decision_payload_for_can
timeouts: resolve_transport_execution_timeouts(transport),
upstream_is_stream,
report_kind: Some(report_kind.to_string()),
report_context: Some(append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": "openai:chat",
"client_api_format": "openai:chat",
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": false,
}),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
"openai:chat",
"openai:chat",
report_context: Some(append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": "openai:chat",
"client_api_format": "openai:chat",
"mapped_model": mapped_model,
"upstream_url": upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": provider_request_headers,
"provider_request_body": provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": false,
"needs_conversion": false,
}),
ExecutionStrategy::LocalSameFormat,
ConversionMode::None,
"openai:chat",
"openai:chat",
),
transport,
)),
auth_context: Some(input.auth_context.clone()),
})
@@ -8,7 +8,8 @@ use crate::ai_pipeline::transport::{
resolve_transport_tls_profile,
};
use crate::{
append_execution_contract_fields_to_value, AppState, GatewayControlSyncDecisionResponse,
append_execution_contract_fields_to_value, append_local_failover_policy_to_value, AppState,
GatewayControlSyncDecisionResponse,
};
use super::request::resolve_local_openai_cli_candidate_payload_parts;
@@ -91,43 +92,46 @@ pub(crate) async fn maybe_build_local_openai_cli_decision_payload_for_candidate(
timeouts: resolve_transport_execution_timeouts(&resolved.transport),
upstream_is_stream: resolved.upstream_is_stream,
report_kind: Some(spec.report_kind.to_string()),
report_context: Some(append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": resolved.transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": resolved.provider_api_format,
"client_api_format": spec.api_format,
"mapped_model": resolved.mapped_model,
"upstream_url": resolved.upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": resolved.provider_request_headers,
"provider_request_body": resolved.provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": resolved.is_antigravity,
"envelope_name": if resolved.is_antigravity {
Some("antigravity:v1internal")
} else {
None
},
"needs_conversion": matches!(resolved.conversion_mode, crate::ai_pipeline::ConversionMode::Bidirectional),
}),
resolved.execution_strategy,
resolved.conversion_mode,
spec.api_format,
candidate.endpoint_api_format.as_str(),
report_context: Some(append_local_failover_policy_to_value(
append_execution_contract_fields_to_value(
json!({
"user_id": input.auth_context.user_id,
"api_key_id": input.auth_context.api_key_id,
"username": input.auth_context.username,
"api_key_name": input.auth_context.api_key_name,
"request_id": trace_id,
"candidate_id": candidate_id,
"candidate_index": candidate_index,
"retry_index": 0,
"model": input.requested_model,
"provider_name": resolved.transport.provider.name,
"provider_id": candidate.provider_id,
"endpoint_id": candidate.endpoint_id,
"key_id": candidate.key_id,
"key_name": candidate.key_name,
"provider_api_format": resolved.provider_api_format,
"client_api_format": spec.api_format,
"mapped_model": resolved.mapped_model,
"upstream_url": resolved.upstream_url,
"provider_request_method": serde_json::Value::Null,
"provider_request_headers": resolved.provider_request_headers,
"provider_request_body": resolved.provider_request_body,
"original_headers": collect_control_headers(&parts.headers),
"original_request_body": body_json,
"has_envelope": resolved.is_antigravity,
"envelope_name": if resolved.is_antigravity {
Some("antigravity:v1internal")
} else {
None
},
"needs_conversion": matches!(resolved.conversion_mode, crate::ai_pipeline::ConversionMode::Bidirectional),
}),
resolved.execution_strategy,
resolved.conversion_mode,
spec.api_format,
candidate.endpoint_api_format.as_str(),
),
&resolved.transport,
)),
auth_context: Some(input.auth_context.clone()),
})