feat(transport): 暴露同格式 provider 的细分失败原因并在时间线展示

- claude_code/kiro/vertex policy 新增 transport_unsupported_reason_with_network 变体,返回具体失败码
- planner candidate_metadata 在 request_pair 写入同格式 provider 的细分 transport 原因
- 前端请求时间线优先展示 transport_diagnostics 的细分原因而非通用 transport_unsupported
This commit is contained in:
fawney19
2026-04-19 21:45:57 +08:00
parent 77aac74590
commit 1f74e660de
14 changed files with 440 additions and 52 deletions

View File

@@ -3,6 +3,7 @@ use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use serde_json::{json, Map, Value};
use crate::ai_pipeline::planner::candidate_eligibility::EligibleLocalExecutionCandidate;
use crate::ai_pipeline::planner::passthrough::resolve_same_format_provider_transport_unsupported_reason_for_trace;
use crate::ai_pipeline::transport::{
body_rules_are_locally_supported, header_rules_are_locally_supported,
local_gemini_transport_unsupported_reason_with_network,
@@ -343,6 +344,14 @@ fn resolve_request_transport_unsupported_reason(
let client_api_format = client_api_format.trim().to_ascii_lowercase();
let provider_api_format = provider_api_format.trim().to_ascii_lowercase();
if client_api_format == provider_api_format {
if let Some(skip_reason) =
resolve_same_format_provider_transport_unsupported_reason_for_trace(
transport,
provider_api_format.as_str(),
)
{
return Some(skip_reason);
}
return match provider_api_format.as_str() {
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
"gemini:chat" | "gemini:cli" => local_gemini_transport_unsupported_reason_with_network(
@@ -456,6 +465,59 @@ mod tests {
}
}
fn sample_claude_code_transport_without_auth() -> GatewayProviderTransportSnapshot {
GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-cc-1".to_string(),
name: "NekoCode".to_string(),
provider_type: "claude_code".to_string(),
website: Some("https://nekocode.ai".to_string()),
is_active: true,
keep_priority_on_conversion: false,
enable_format_conversion: true,
concurrent_limit: None,
max_retries: None,
proxy: None,
request_timeout_secs: None,
stream_first_byte_timeout_secs: None,
config: None,
},
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
api_format: "claude:cli".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
base_url: "https://api.anthropic.com".to_string(),
header_rules: None,
body_rules: None,
max_retries: None,
custom_path: None,
config: None,
format_acceptance_config: None,
proxy: None,
},
key: GatewayProviderTransportKey {
id: "key-cc-1".to_string(),
provider_id: "provider-cc-1".to_string(),
name: "CC-特价-0.4".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
global_priority_by_format: None,
expires_at_unix_secs: None,
proxy: None,
fingerprint: None,
decrypted_api_key: "__placeholder__".to_string(),
decrypted_auth_config: None,
},
}
}
#[test]
fn candidate_contract_metadata_includes_transport_diagnostics() {
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
@@ -503,4 +565,20 @@ mod tests {
Value::Bool(false)
);
}
#[test]
fn candidate_metadata_uses_same_format_provider_specific_transport_reason() {
let metadata = build_local_execution_candidate_metadata_for_candidate(
&sample_candidate(),
Some(&sample_claude_code_transport_without_auth()),
"claude:cli",
"claude:cli",
serde_json::Map::new(),
);
assert_eq!(
metadata["transport_diagnostics"]["request_pair"]["transport_unsupported_reason"],
Value::String("transport_auth_unavailable".to_string())
);
}
}

View File

@@ -7,5 +7,6 @@ pub(crate) use self::provider::{
build_local_sync_plan_and_reports as build_local_same_format_sync_plan_and_reports,
maybe_build_stream_local_same_format_provider_decision_payload,
maybe_build_sync_local_same_format_provider_decision_payload,
resolve_same_format_provider_transport_unsupported_reason_for_trace,
};
pub(crate) use crate::ai_pipeline::transport::provider_types::provider_type_supports_local_same_format_transport;

View File

@@ -65,11 +65,12 @@ mod family;
mod plans;
mod request;
pub(super) use self::family::{
pub(crate) use self::family::{
materialize_local_same_format_provider_candidate_attempts,
maybe_build_local_same_format_provider_decision_payload_for_candidate,
resolve_local_same_format_provider_decision_input, LocalSameFormatProviderFamily,
LocalSameFormatProviderSpec,
resolve_local_same_format_provider_decision_input,
resolve_same_format_provider_transport_unsupported_reason_for_trace,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
};
pub(crate) use self::family::{
maybe_build_stream_local_same_format_provider_decision_payload,

View File

@@ -12,6 +12,7 @@ pub(crate) use self::candidates::{
resolve_local_same_format_provider_decision_input,
};
pub(crate) use self::payload::maybe_build_local_same_format_provider_decision_payload_for_candidate;
pub(crate) use self::request::resolve_same_format_provider_transport_unsupported_reason_for_trace;
pub(crate) use crate::ai_pipeline::planner::candidate_materialization::LocalExecutionCandidateAttempt as LocalSameFormatProviderCandidateAttempt;
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalSameFormatProviderDecisionInput;
pub(crate) use crate::ai_pipeline::{LocalSameFormatProviderFamily, LocalSameFormatProviderSpec};

View File

@@ -26,6 +26,51 @@ use super::{
LocalSameFormatProviderSpec,
};
pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trace(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> Option<&'static str> {
let provider_api_format = match provider_api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => "openai:chat",
"openai:cli" => "openai:cli",
"openai:compact" => "openai:compact",
"claude:chat" => "claude:chat",
"claude:cli" => "claude:cli",
"gemini:chat" => "gemini:chat",
"gemini:cli" => "gemini:cli",
_ => return Some("transport_api_format_unsupported"),
};
let behavior = policy::classify_same_format_provider_request_behavior(
transport,
crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata {
api_format: provider_api_format,
require_streaming: false,
requested_model_family: None,
decision_kind: "trace_candidate_metadata",
report_kind: Some("trace_candidate_metadata"),
},
);
if !behavior.is_antigravity
&& !behavior.is_claude_code
&& !behavior.is_vertex
&& !behavior.is_kiro
{
return None;
}
let family = if provider_api_format.starts_with("gemini:") {
crate::ai_pipeline::LocalSameFormatProviderFamily::Gemini
} else {
crate::ai_pipeline::LocalSameFormatProviderFamily::Standard
};
policy::same_format_provider_transport_unsupported_reason(
&behavior,
transport,
family,
provider_api_format,
)
}
pub(crate) struct LocalSameFormatProviderCandidatePayloadParts {
pub(super) transport: GatewayProviderTransportSnapshot,
pub(super) is_antigravity: bool,

View File

@@ -1,11 +1,12 @@
use crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata;
use crate::ai_pipeline::transport::auth::{resolve_local_gemini_auth, resolve_local_standard_auth};
use crate::ai_pipeline::transport::claude_code::supports_local_claude_code_transport_with_network;
use crate::ai_pipeline::transport::kiro::supports_local_kiro_request_transport_with_network;
use crate::ai_pipeline::transport::claude_code::local_claude_code_transport_unsupported_reason_with_network;
use crate::ai_pipeline::transport::kiro::local_kiro_request_transport_unsupported_reason_with_network;
use crate::ai_pipeline::transport::policy::{
supports_local_gemini_transport_with_network, supports_local_standard_transport_with_network,
local_gemini_transport_unsupported_reason_with_network,
local_standard_transport_unsupported_reason_with_network,
};
use crate::ai_pipeline::transport::vertex::supports_local_vertex_api_key_gemini_transport_with_network;
use crate::ai_pipeline::transport::vertex::local_vertex_api_key_gemini_transport_unsupported_reason_with_network;
use crate::ai_pipeline::GatewayProviderTransportSnapshot;
use super::super::LocalSameFormatProviderFamily;
@@ -75,21 +76,31 @@ pub(super) fn same_format_provider_transport_supported(
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> bool {
same_format_provider_transport_unsupported_reason(behavior, transport, family, api_format)
.is_none()
}
pub(super) fn same_format_provider_transport_unsupported_reason(
behavior: &SameFormatProviderRequestBehavior,
transport: &GatewayProviderTransportSnapshot,
family: LocalSameFormatProviderFamily,
api_format: &str,
) -> Option<&'static str> {
if behavior.is_kiro {
supports_local_kiro_request_transport_with_network(transport)
local_kiro_request_transport_unsupported_reason_with_network(transport)
} else if behavior.is_antigravity {
true
None
} else if behavior.is_claude_code {
supports_local_claude_code_transport_with_network(transport, api_format)
local_claude_code_transport_unsupported_reason_with_network(transport, api_format)
} else if behavior.is_vertex {
supports_local_vertex_api_key_gemini_transport_with_network(transport)
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(transport)
} else {
match family {
LocalSameFormatProviderFamily::Standard => {
supports_local_standard_transport_with_network(transport, api_format)
local_standard_transport_unsupported_reason_with_network(transport, api_format)
}
LocalSameFormatProviderFamily::Gemini => {
supports_local_gemini_transport_with_network(transport, api_format)
local_gemini_transport_unsupported_reason_with_network(transport, api_format)
}
}
}

View File

@@ -14,7 +14,8 @@ use super::super::LocalSameFormatProviderDecisionInput;
use super::super::LocalSameFormatProviderSpec;
use super::policy::{
classify_same_format_provider_request_behavior, resolve_same_format_provider_direct_auth,
same_format_provider_transport_supported, should_try_same_format_provider_oauth_auth,
same_format_provider_transport_supported, same_format_provider_transport_unsupported_reason,
should_try_same_format_provider_oauth_auth,
};
pub(super) struct PreparedSameFormatProviderCandidate {
@@ -52,6 +53,13 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
spec.family,
spec_metadata.api_format,
) {
let skip_reason = same_format_provider_transport_unsupported_reason(
&behavior,
&transport,
spec.family,
spec_metadata.api_format,
)
.unwrap_or("transport_unsupported");
super::super::payload::mark_skipped_local_same_format_provider_candidate(
state,
input,
@@ -59,7 +67,7 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
candidate,
candidate_index,
candidate_id,
"transport_unsupported",
skip_reason,
)
.await;
return None;