mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +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:
@@ -6,6 +6,9 @@ mod url;
|
||||
|
||||
pub use auth::supports_local_claude_code_auth;
|
||||
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 url::build_claude_code_messages_url;
|
||||
|
||||
@@ -6,12 +6,18 @@ use super::super::{
|
||||
};
|
||||
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,
|
||||
api_format: &str,
|
||||
) -> bool {
|
||||
) -> Option<&'static str> {
|
||||
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
|
||||
.provider
|
||||
@@ -19,7 +25,7 @@ pub fn supports_local_claude_code_transport_with_network(
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude_code")
|
||||
{
|
||||
return false;
|
||||
return Some("transport_provider_type_unsupported");
|
||||
}
|
||||
if !transport
|
||||
.endpoint
|
||||
@@ -27,27 +33,122 @@ pub fn supports_local_claude_code_transport_with_network(
|
||||
.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())
|
||||
|| !body_rules_are_locally_supported(transport.endpoint.body_rules.as_ref())
|
||||
{
|
||||
return false;
|
||||
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
|
||||
return Some("transport_header_rules_unsupported");
|
||||
}
|
||||
if !supports_local_claude_code_auth(transport) {
|
||||
return false;
|
||||
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_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) {
|
||||
return false;
|
||||
return Some("transport_proxy_unsupported");
|
||||
}
|
||||
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 headers::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
|
||||
pub use policy::{
|
||||
local_kiro_request_transport_unsupported_reason_with_network,
|
||||
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
||||
};
|
||||
pub use refresh::KiroOAuthRefreshAdapter;
|
||||
|
||||
@@ -1,11 +1,74 @@
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
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 {
|
||||
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||
return false;
|
||||
}
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(PROVIDER_TYPE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !transport
|
||||
.endpoint
|
||||
.api_format
|
||||
@@ -14,26 +77,16 @@ pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTranspor
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if !supports_local_kiro_request_auth_resolution(transport) {
|
||||
return false;
|
||||
}
|
||||
if !supports_local_kiro_request_shape(
|
||||
supports_local_kiro_request_shape(
|
||||
transport.endpoint.header_rules.as_ref(),
|
||||
transport.endpoint.body_rules.as_ref(),
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
) && supports_local_kiro_request_auth_resolution(transport)
|
||||
}
|
||||
|
||||
pub fn supports_local_kiro_request_transport_with_network(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> bool {
|
||||
supports_local_kiro_request_transport(transport)
|
||||
&& transport_proxy_is_locally_supported(transport)
|
||||
&& (transport.key.fingerprint.is_none()
|
||||
|| resolve_transport_tls_profile(transport).is_some())
|
||||
local_kiro_request_transport_unsupported_reason_with_network(transport).is_none()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -43,6 +96,7 @@ mod tests {
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
use super::{
|
||||
local_kiro_request_transport_unsupported_reason_with_network,
|
||||
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
|
||||
};
|
||||
|
||||
@@ -130,4 +184,16 @@ mod tests {
|
||||
&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,
|
||||
};
|
||||
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_with_network,
|
||||
supports_local_vertex_api_key_imagen_transport,
|
||||
|
||||
@@ -5,6 +5,51 @@ use super::super::{
|
||||
};
|
||||
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(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> 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(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> bool {
|
||||
supports_local_vertex_api_key_same_format_transport(
|
||||
transport,
|
||||
&["gemini:chat", "gemini:cli"],
|
||||
true,
|
||||
)
|
||||
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(transport).is_none()
|
||||
}
|
||||
|
||||
pub fn supports_local_vertex_api_key_imagen_transport(
|
||||
@@ -110,6 +151,7 @@ mod tests {
|
||||
};
|
||||
|
||||
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_with_network,
|
||||
};
|
||||
@@ -201,4 +243,16 @@ mod tests {
|
||||
&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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user