mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
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:
@@ -3,6 +3,7 @@ use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
|
|||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
use crate::ai_pipeline::planner::candidate_eligibility::EligibleLocalExecutionCandidate;
|
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::{
|
use crate::ai_pipeline::transport::{
|
||||||
body_rules_are_locally_supported, header_rules_are_locally_supported,
|
body_rules_are_locally_supported, header_rules_are_locally_supported,
|
||||||
local_gemini_transport_unsupported_reason_with_network,
|
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 client_api_format = client_api_format.trim().to_ascii_lowercase();
|
||||||
let provider_api_format = provider_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 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() {
|
return match provider_api_format.as_str() {
|
||||||
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
|
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
|
||||||
"gemini:chat" | "gemini:cli" => local_gemini_transport_unsupported_reason_with_network(
|
"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]
|
#[test]
|
||||||
fn candidate_contract_metadata_includes_transport_diagnostics() {
|
fn candidate_contract_metadata_includes_transport_diagnostics() {
|
||||||
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
|
let metadata = build_local_execution_candidate_contract_metadata_for_candidate(
|
||||||
@@ -503,4 +565,20 @@ mod tests {
|
|||||||
Value::Bool(false)
|
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())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ pub(crate) use self::provider::{
|
|||||||
build_local_sync_plan_and_reports as build_local_same_format_sync_plan_and_reports,
|
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_stream_local_same_format_provider_decision_payload,
|
||||||
maybe_build_sync_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;
|
pub(crate) use crate::ai_pipeline::transport::provider_types::provider_type_supports_local_same_format_transport;
|
||||||
|
|||||||
@@ -65,11 +65,12 @@ mod family;
|
|||||||
mod plans;
|
mod plans;
|
||||||
mod request;
|
mod request;
|
||||||
|
|
||||||
pub(super) use self::family::{
|
pub(crate) use self::family::{
|
||||||
materialize_local_same_format_provider_candidate_attempts,
|
materialize_local_same_format_provider_candidate_attempts,
|
||||||
maybe_build_local_same_format_provider_decision_payload_for_candidate,
|
maybe_build_local_same_format_provider_decision_payload_for_candidate,
|
||||||
resolve_local_same_format_provider_decision_input, LocalSameFormatProviderFamily,
|
resolve_local_same_format_provider_decision_input,
|
||||||
LocalSameFormatProviderSpec,
|
resolve_same_format_provider_transport_unsupported_reason_for_trace,
|
||||||
|
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec,
|
||||||
};
|
};
|
||||||
pub(crate) use self::family::{
|
pub(crate) use self::family::{
|
||||||
maybe_build_stream_local_same_format_provider_decision_payload,
|
maybe_build_stream_local_same_format_provider_decision_payload,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub(crate) use self::candidates::{
|
|||||||
resolve_local_same_format_provider_decision_input,
|
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::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::candidate_materialization::LocalExecutionCandidateAttempt as LocalSameFormatProviderCandidateAttempt;
|
||||||
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalSameFormatProviderDecisionInput;
|
pub(crate) use crate::ai_pipeline::planner::decision_input::LocalRequestedModelDecisionInput as LocalSameFormatProviderDecisionInput;
|
||||||
pub(crate) use crate::ai_pipeline::{LocalSameFormatProviderFamily, LocalSameFormatProviderSpec};
|
pub(crate) use crate::ai_pipeline::{LocalSameFormatProviderFamily, LocalSameFormatProviderSpec};
|
||||||
|
|||||||
@@ -26,6 +26,51 @@ use super::{
|
|||||||
LocalSameFormatProviderSpec,
|
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(crate) struct LocalSameFormatProviderCandidatePayloadParts {
|
||||||
pub(super) transport: GatewayProviderTransportSnapshot,
|
pub(super) transport: GatewayProviderTransportSnapshot,
|
||||||
pub(super) is_antigravity: bool,
|
pub(super) is_antigravity: bool,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::ai_pipeline::planner::spec_metadata::LocalExecutionSurfaceSpecMetadata;
|
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::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::claude_code::local_claude_code_transport_unsupported_reason_with_network;
|
||||||
use crate::ai_pipeline::transport::kiro::supports_local_kiro_request_transport_with_network;
|
use crate::ai_pipeline::transport::kiro::local_kiro_request_transport_unsupported_reason_with_network;
|
||||||
use crate::ai_pipeline::transport::policy::{
|
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 crate::ai_pipeline::GatewayProviderTransportSnapshot;
|
||||||
|
|
||||||
use super::super::LocalSameFormatProviderFamily;
|
use super::super::LocalSameFormatProviderFamily;
|
||||||
@@ -75,21 +76,31 @@ pub(super) fn same_format_provider_transport_supported(
|
|||||||
family: LocalSameFormatProviderFamily,
|
family: LocalSameFormatProviderFamily,
|
||||||
api_format: &str,
|
api_format: &str,
|
||||||
) -> bool {
|
) -> 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 {
|
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 {
|
} else if behavior.is_antigravity {
|
||||||
true
|
None
|
||||||
} else if behavior.is_claude_code {
|
} 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 {
|
} 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 {
|
} else {
|
||||||
match family {
|
match family {
|
||||||
LocalSameFormatProviderFamily::Standard => {
|
LocalSameFormatProviderFamily::Standard => {
|
||||||
supports_local_standard_transport_with_network(transport, api_format)
|
local_standard_transport_unsupported_reason_with_network(transport, api_format)
|
||||||
}
|
}
|
||||||
LocalSameFormatProviderFamily::Gemini => {
|
LocalSameFormatProviderFamily::Gemini => {
|
||||||
supports_local_gemini_transport_with_network(transport, api_format)
|
local_gemini_transport_unsupported_reason_with_network(transport, api_format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ use super::super::LocalSameFormatProviderDecisionInput;
|
|||||||
use super::super::LocalSameFormatProviderSpec;
|
use super::super::LocalSameFormatProviderSpec;
|
||||||
use super::policy::{
|
use super::policy::{
|
||||||
classify_same_format_provider_request_behavior, resolve_same_format_provider_direct_auth,
|
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 {
|
pub(super) struct PreparedSameFormatProviderCandidate {
|
||||||
@@ -52,6 +53,13 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
|
|||||||
spec.family,
|
spec.family,
|
||||||
spec_metadata.api_format,
|
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(
|
super::super::payload::mark_skipped_local_same_format_provider_candidate(
|
||||||
state,
|
state,
|
||||||
input,
|
input,
|
||||||
@@ -59,7 +67,7 @@ pub(super) async fn prepare_local_same_format_provider_candidate(
|
|||||||
candidate,
|
candidate,
|
||||||
candidate_index,
|
candidate_index,
|
||||||
candidate_id,
|
candidate_id,
|
||||||
"transport_unsupported",
|
skip_reason,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
return None;
|
return None;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ mod url;
|
|||||||
|
|
||||||
pub use auth::supports_local_claude_code_auth;
|
pub use auth::supports_local_claude_code_auth;
|
||||||
pub use fingerprint::{generate_fingerprint, generate_random_fingerprint, sanitize_fingerprint};
|
pub use fingerprint::{generate_fingerprint, generate_random_fingerprint, sanitize_fingerprint};
|
||||||
pub use policy::supports_local_claude_code_transport_with_network;
|
pub use policy::{
|
||||||
|
local_claude_code_transport_unsupported_reason_with_network,
|
||||||
|
supports_local_claude_code_transport_with_network,
|
||||||
|
};
|
||||||
pub use request::{build_claude_code_passthrough_headers, sanitize_claude_code_request_body};
|
pub use request::{build_claude_code_passthrough_headers, sanitize_claude_code_request_body};
|
||||||
pub use url::build_claude_code_messages_url;
|
pub use url::build_claude_code_messages_url;
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ use super::super::{
|
|||||||
};
|
};
|
||||||
use super::auth::supports_local_claude_code_auth;
|
use super::auth::supports_local_claude_code_auth;
|
||||||
|
|
||||||
pub fn supports_local_claude_code_transport_with_network(
|
pub fn local_claude_code_transport_unsupported_reason_with_network(
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
api_format: &str,
|
api_format: &str,
|
||||||
) -> bool {
|
) -> Option<&'static str> {
|
||||||
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||||
return false;
|
return if !transport.provider.is_active {
|
||||||
|
Some("provider_inactive")
|
||||||
|
} else if !transport.endpoint.is_active {
|
||||||
|
Some("endpoint_inactive")
|
||||||
|
} else {
|
||||||
|
Some("key_inactive")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if !transport
|
if !transport
|
||||||
.provider
|
.provider
|
||||||
@@ -19,7 +25,7 @@ pub fn supports_local_claude_code_transport_with_network(
|
|||||||
.trim()
|
.trim()
|
||||||
.eq_ignore_ascii_case("claude_code")
|
.eq_ignore_ascii_case("claude_code")
|
||||||
{
|
{
|
||||||
return false;
|
return Some("transport_provider_type_unsupported");
|
||||||
}
|
}
|
||||||
if !transport
|
if !transport
|
||||||
.endpoint
|
.endpoint
|
||||||
@@ -27,27 +33,122 @@ pub fn supports_local_claude_code_transport_with_network(
|
|||||||
.trim()
|
.trim()
|
||||||
.eq_ignore_ascii_case(api_format.trim())
|
.eq_ignore_ascii_case(api_format.trim())
|
||||||
{
|
{
|
||||||
return false;
|
return Some("transport_api_format_mismatch");
|
||||||
}
|
}
|
||||||
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref())
|
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
|
||||||
|| !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref())
|
return Some("transport_header_rules_unsupported");
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if !supports_local_claude_code_auth(transport) {
|
if !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref()) {
|
||||||
return false;
|
return Some("transport_body_rules_unsupported");
|
||||||
}
|
}
|
||||||
if transport.key.decrypted_auth_config.is_some()
|
if transport.key.decrypted_auth_config.is_some()
|
||||||
&& !supports_local_oauth_request_auth_resolution(transport)
|
&& !supports_local_oauth_request_auth_resolution(transport)
|
||||||
{
|
{
|
||||||
return false;
|
return Some("transport_oauth_resolution_unsupported");
|
||||||
|
}
|
||||||
|
if !supports_local_claude_code_auth(transport) {
|
||||||
|
return Some("transport_auth_unavailable");
|
||||||
}
|
}
|
||||||
if !transport_proxy_is_locally_supported(transport) {
|
if !transport_proxy_is_locally_supported(transport) {
|
||||||
return false;
|
return Some("transport_proxy_unsupported");
|
||||||
}
|
}
|
||||||
if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() {
|
if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() {
|
||||||
return false;
|
return Some("transport_tls_profile_unsupported");
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn supports_local_claude_code_transport_with_network(
|
||||||
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
|
api_format: &str,
|
||||||
|
) -> bool {
|
||||||
|
local_claude_code_transport_unsupported_reason_with_network(transport, api_format).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::super::super::snapshot::{
|
||||||
|
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||||
|
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||||
|
};
|
||||||
|
use super::{
|
||||||
|
local_claude_code_transport_unsupported_reason_with_network,
|
||||||
|
supports_local_claude_code_transport_with_network,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||||
|
GatewayProviderTransportSnapshot {
|
||||||
|
provider: GatewayProviderTransportProvider {
|
||||||
|
id: "provider-1".to_string(),
|
||||||
|
name: "Claude Code".to_string(),
|
||||||
|
provider_type: "claude_code".to_string(),
|
||||||
|
website: None,
|
||||||
|
is_active: true,
|
||||||
|
keep_priority_on_conversion: false,
|
||||||
|
enable_format_conversion: false,
|
||||||
|
concurrent_limit: None,
|
||||||
|
max_retries: None,
|
||||||
|
proxy: None,
|
||||||
|
request_timeout_secs: None,
|
||||||
|
stream_first_byte_timeout_secs: None,
|
||||||
|
config: None,
|
||||||
|
},
|
||||||
|
endpoint: GatewayProviderTransportEndpoint {
|
||||||
|
id: "endpoint-1".to_string(),
|
||||||
|
provider_id: "provider-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-1".to_string(),
|
||||||
|
provider_id: "provider-1".to_string(),
|
||||||
|
name: "key".to_string(),
|
||||||
|
auth_type: "bearer".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: Some(json!({"tls_profile":"chrome_136"})),
|
||||||
|
decrypted_api_key: "sk-ant-123".to_string(),
|
||||||
|
decrypted_auth_config: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supports_claude_code_transport_when_auth_and_tls_are_valid() {
|
||||||
|
assert!(supports_local_claude_code_transport_with_network(
|
||||||
|
&sample_transport(),
|
||||||
|
"claude:cli"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_auth_unavailable_for_claude_code_without_local_auth() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.key.auth_type = "api_key".to_string();
|
||||||
|
transport.key.decrypted_api_key = "__placeholder__".to_string();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
local_claude_code_transport_unsupported_reason_with_network(&transport, "claude:cli"),
|
||||||
|
Some("transport_auth_unavailable")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub use converter::convert_claude_messages_to_conversation_state;
|
|||||||
pub use credentials::{generate_machine_id, normalize_machine_id, KiroAuthConfig};
|
pub use credentials::{generate_machine_id, normalize_machine_id, KiroAuthConfig};
|
||||||
pub use headers::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
|
pub use headers::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
|
||||||
pub use policy::{
|
pub use policy::{
|
||||||
|
local_kiro_request_transport_unsupported_reason_with_network,
|
||||||
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
||||||
};
|
};
|
||||||
pub use refresh::KiroOAuthRefreshAdapter;
|
pub use refresh::KiroOAuthRefreshAdapter;
|
||||||
|
|||||||
@@ -1,11 +1,74 @@
|
|||||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||||
use super::super::{resolve_transport_tls_profile, transport_proxy_is_locally_supported};
|
use super::super::{resolve_transport_tls_profile, transport_proxy_is_locally_supported};
|
||||||
use super::{supports_local_kiro_request_auth_resolution, supports_local_kiro_request_shape};
|
use super::{
|
||||||
|
body_rules_are_locally_supported, header_rules_are_locally_supported,
|
||||||
|
supports_local_kiro_request_auth_resolution, supports_local_kiro_request_shape, PROVIDER_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn local_kiro_request_transport_unsupported_reason_with_network(
|
||||||
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||||
|
return if !transport.provider.is_active {
|
||||||
|
Some("provider_inactive")
|
||||||
|
} else if !transport.endpoint.is_active {
|
||||||
|
Some("endpoint_inactive")
|
||||||
|
} else {
|
||||||
|
Some("key_inactive")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !transport
|
||||||
|
.provider
|
||||||
|
.provider_type
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case(PROVIDER_TYPE)
|
||||||
|
{
|
||||||
|
return Some("transport_provider_type_unsupported");
|
||||||
|
}
|
||||||
|
if !transport
|
||||||
|
.endpoint
|
||||||
|
.api_format
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case("claude:cli")
|
||||||
|
{
|
||||||
|
return Some("transport_api_format_mismatch");
|
||||||
|
}
|
||||||
|
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
|
||||||
|
return Some("transport_header_rules_unsupported");
|
||||||
|
}
|
||||||
|
if !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref()) {
|
||||||
|
return Some("transport_body_rules_unsupported");
|
||||||
|
}
|
||||||
|
if transport.key.decrypted_auth_config.is_some()
|
||||||
|
&& !supports_local_kiro_request_auth_resolution(transport)
|
||||||
|
{
|
||||||
|
return Some("transport_oauth_resolution_unsupported");
|
||||||
|
}
|
||||||
|
if !supports_local_kiro_request_auth_resolution(transport) {
|
||||||
|
return Some("transport_auth_unavailable");
|
||||||
|
}
|
||||||
|
if !transport_proxy_is_locally_supported(transport) {
|
||||||
|
return Some("transport_proxy_unsupported");
|
||||||
|
}
|
||||||
|
if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() {
|
||||||
|
return Some("transport_tls_profile_unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
|
pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||||
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if !transport
|
||||||
|
.provider
|
||||||
|
.provider_type
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case(PROVIDER_TYPE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if !transport
|
if !transport
|
||||||
.endpoint
|
.endpoint
|
||||||
.api_format
|
.api_format
|
||||||
@@ -14,26 +77,16 @@ pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTranspor
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if !supports_local_kiro_request_auth_resolution(transport) {
|
supports_local_kiro_request_shape(
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if !supports_local_kiro_request_shape(
|
|
||||||
transport.endpoint.header_rules.as_ref(),
|
transport.endpoint.header_rules.as_ref(),
|
||||||
transport.endpoint.body_rules.as_ref(),
|
transport.endpoint.body_rules.as_ref(),
|
||||||
) {
|
) && supports_local_kiro_request_auth_resolution(transport)
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn supports_local_kiro_request_transport_with_network(
|
pub fn supports_local_kiro_request_transport_with_network(
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
supports_local_kiro_request_transport(transport)
|
local_kiro_request_transport_unsupported_reason_with_network(transport).is_none()
|
||||||
&& transport_proxy_is_locally_supported(transport)
|
|
||||||
&& (transport.key.fingerprint.is_none()
|
|
||||||
|| resolve_transport_tls_profile(transport).is_some())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -43,6 +96,7 @@ mod tests {
|
|||||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
|
local_kiro_request_transport_unsupported_reason_with_network,
|
||||||
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,4 +184,16 @@ mod tests {
|
|||||||
&transport
|
&transport
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_auth_unavailable_when_kiro_key_cannot_resolve_request_auth() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.key.auth_type = "api_key".to_string();
|
||||||
|
transport.key.decrypted_auth_config = None;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
local_kiro_request_transport_unsupported_reason_with_network(&transport),
|
||||||
|
Some("transport_auth_unavailable")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub use auth::{
|
|||||||
resolve_local_vertex_api_key_query_auth, VertexApiKeyQueryAuth, VERTEX_API_KEY_QUERY_PARAM,
|
resolve_local_vertex_api_key_query_auth, VertexApiKeyQueryAuth, VERTEX_API_KEY_QUERY_PARAM,
|
||||||
};
|
};
|
||||||
pub use policy::{
|
pub use policy::{
|
||||||
|
local_vertex_api_key_gemini_transport_unsupported_reason_with_network,
|
||||||
supports_local_vertex_api_key_gemini_transport,
|
supports_local_vertex_api_key_gemini_transport,
|
||||||
supports_local_vertex_api_key_gemini_transport_with_network,
|
supports_local_vertex_api_key_gemini_transport_with_network,
|
||||||
supports_local_vertex_api_key_imagen_transport,
|
supports_local_vertex_api_key_imagen_transport,
|
||||||
|
|||||||
@@ -5,6 +5,51 @@ use super::super::{
|
|||||||
};
|
};
|
||||||
use super::auth::resolve_local_vertex_api_key_query_auth;
|
use super::auth::resolve_local_vertex_api_key_query_auth;
|
||||||
|
|
||||||
|
pub fn local_vertex_api_key_gemini_transport_unsupported_reason_with_network(
|
||||||
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||||
|
return if !transport.provider.is_active {
|
||||||
|
Some("provider_inactive")
|
||||||
|
} else if !transport.endpoint.is_active {
|
||||||
|
Some("endpoint_inactive")
|
||||||
|
} else {
|
||||||
|
Some("key_inactive")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if !transport
|
||||||
|
.provider
|
||||||
|
.provider_type
|
||||||
|
.trim()
|
||||||
|
.eq_ignore_ascii_case(super::PROVIDER_TYPE)
|
||||||
|
{
|
||||||
|
return Some("transport_provider_type_unsupported");
|
||||||
|
}
|
||||||
|
let endpoint_api_format = transport.endpoint.api_format.trim();
|
||||||
|
if !endpoint_api_format.eq_ignore_ascii_case("gemini:chat")
|
||||||
|
&& !endpoint_api_format.eq_ignore_ascii_case("gemini:cli")
|
||||||
|
{
|
||||||
|
return Some("transport_api_format_mismatch");
|
||||||
|
}
|
||||||
|
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
|
||||||
|
return Some("transport_header_rules_unsupported");
|
||||||
|
}
|
||||||
|
if !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref()) {
|
||||||
|
return Some("transport_body_rules_unsupported");
|
||||||
|
}
|
||||||
|
if resolve_local_vertex_api_key_query_auth(transport).is_none() {
|
||||||
|
return Some("transport_auth_unavailable");
|
||||||
|
}
|
||||||
|
if !transport_proxy_is_locally_supported(transport) {
|
||||||
|
return Some("transport_proxy_unsupported");
|
||||||
|
}
|
||||||
|
if transport.key.fingerprint.is_some() && resolve_transport_tls_profile(transport).is_none() {
|
||||||
|
return Some("transport_tls_profile_unsupported");
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn supports_local_vertex_api_key_gemini_transport(
|
pub fn supports_local_vertex_api_key_gemini_transport(
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
@@ -18,11 +63,7 @@ pub fn supports_local_vertex_api_key_gemini_transport(
|
|||||||
pub fn supports_local_vertex_api_key_gemini_transport_with_network(
|
pub fn supports_local_vertex_api_key_gemini_transport_with_network(
|
||||||
transport: &GatewayProviderTransportSnapshot,
|
transport: &GatewayProviderTransportSnapshot,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
supports_local_vertex_api_key_same_format_transport(
|
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(transport).is_none()
|
||||||
transport,
|
|
||||||
&["gemini:chat", "gemini:cli"],
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn supports_local_vertex_api_key_imagen_transport(
|
pub fn supports_local_vertex_api_key_imagen_transport(
|
||||||
@@ -110,6 +151,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
local_vertex_api_key_gemini_transport_unsupported_reason_with_network,
|
||||||
supports_local_vertex_api_key_gemini_transport,
|
supports_local_vertex_api_key_gemini_transport,
|
||||||
supports_local_vertex_api_key_gemini_transport_with_network,
|
supports_local_vertex_api_key_gemini_transport_with_network,
|
||||||
};
|
};
|
||||||
@@ -201,4 +243,16 @@ mod tests {
|
|||||||
&transport
|
&transport
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_auth_unavailable_when_vertex_api_key_query_auth_is_missing() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.key.auth_type = "service_account".to_string();
|
||||||
|
transport.key.decrypted_api_key = "__placeholder__".to_string();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(&transport),
|
||||||
|
Some("transport_auth_unavailable")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,11 +435,11 @@
|
|||||||
|
|
||||||
<!-- 跳过原因 -->
|
<!-- 跳过原因 -->
|
||||||
<div
|
<div
|
||||||
v-if="currentAttempt.skip_reason"
|
v-if="currentAttemptSkipReasonDisplay"
|
||||||
class="skip-reason"
|
class="skip-reason"
|
||||||
>
|
>
|
||||||
<span class="reason-label">跳过原因</span>
|
<span class="reason-label">跳过原因</span>
|
||||||
<span class="reason-value">{{ currentAttempt.skip_reason }}</span>
|
<span class="reason-value">{{ currentAttemptSkipReasonDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 错误信息 -->
|
<!-- 错误信息 -->
|
||||||
@@ -1370,6 +1370,23 @@ const currentAttemptConversionInfo = computed<{
|
|||||||
return { summary, hint }
|
return { summary, hint }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentAttemptSkipReasonDisplay = computed(() => {
|
||||||
|
const attempt = currentAttempt.value
|
||||||
|
if (!attempt?.skip_reason) return ''
|
||||||
|
|
||||||
|
if (attempt.skip_reason !== 'transport_unsupported') {
|
||||||
|
return attempt.skip_reason
|
||||||
|
}
|
||||||
|
|
||||||
|
const transportDiagnostics = resolveTransportDiagnostics(attempt)
|
||||||
|
const requestPair = extractObject(transportDiagnostics?.request_pair)
|
||||||
|
const detailedReason = typeof requestPair?.transport_unsupported_reason === 'string'
|
||||||
|
? requestPair.transport_unsupported_reason.trim()
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return detailedReason || attempt.skip_reason
|
||||||
|
})
|
||||||
|
|
||||||
// 计算当前尝试启用的能力标签(请求需要的能力)
|
// 计算当前尝试启用的能力标签(请求需要的能力)
|
||||||
const activeCapabilities = computed(() => {
|
const activeCapabilities = computed(() => {
|
||||||
if (!currentAttempt.value?.required_capabilities) return []
|
if (!currentAttempt.value?.required_capabilities) return []
|
||||||
|
|||||||
Reference in New Issue
Block a user