mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 03:39:49 +08:00
fix(antigravity): wrap cross-format requests in the v1internal envelope
The gemini:generate_content URL hook rewrites any Antigravity endpoint to /v1internal:generateContent, but only the same-format passthrough and the two OpenAI decision paths ever built the matching envelope. A Claude Messages or Gemini client therefore reached the standard family planner, picked up the rewritten URL, and posted a bare Gemini body that upstream rejects with "Invalid JSON payload received. Unknown name \"contents\"" -- four retries across every account, then a 503 that names none of this. Route the standard family through the shared v1internal builder the same way gemini_cli already is, so the URL and the body come from one decision. The OpenAI-image-to-Gemini path cannot carry an envelope at all, so it now skips Antigravity candidates instead of sending a request upstream can only reject. Also stop treating a configured proxy as locally unsupported. The execution plan carries the proxy itself, and the generic and Vertex gates moved to transport_proxy_is_locally_supported long ago; Antigravity kept rejecting on proxy.is_some(), which no longer matches how the local runtime executes. A proxy that resolves to no route still disqualifies the request, and transport profiles stay unsupported because the v1internal payload never carries one. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::ai_serving::planner::candidate_preparation::{
|
|||||||
};
|
};
|
||||||
use crate::ai_serving::planner::spec_metadata::local_openai_image_spec_metadata;
|
use crate::ai_serving::planner::spec_metadata::local_openai_image_spec_metadata;
|
||||||
use crate::ai_serving::pure::normalize_openai_image_request_with_options;
|
use crate::ai_serving::pure::normalize_openai_image_request_with_options;
|
||||||
|
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
||||||
use crate::ai_serving::transport::{
|
use crate::ai_serving::transport::{
|
||||||
build_grok_browser_headers, build_grok_upstream_url, build_openai_image_headers,
|
build_grok_browser_headers, build_grok_upstream_url, build_openai_image_headers,
|
||||||
build_openai_image_upstream_url, build_standard_provider_request_headers,
|
build_openai_image_upstream_url, build_standard_provider_request_headers,
|
||||||
@@ -338,6 +339,25 @@ async fn resolve_local_openai_image_to_gemini_candidate_payload_parts(
|
|||||||
let candidate = &attempt.eligible.candidate;
|
let candidate = &attempt.eligible.candidate;
|
||||||
let transport = &attempt.eligible.transport;
|
let transport = &attempt.eligible.transport;
|
||||||
let provider_api_format = "gemini:generate_content";
|
let provider_api_format = "gemini:generate_content";
|
||||||
|
|
||||||
|
// The gemini:generate_content URL hook rewrites an Antigravity endpoint to
|
||||||
|
// /v1internal:, and this image path has no v1internal envelope to match it.
|
||||||
|
// Skip the candidate instead of posting a bare Gemini body that upstream
|
||||||
|
// would only reject.
|
||||||
|
if is_antigravity_provider_transport(transport) {
|
||||||
|
mark_skipped_local_openai_image_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"transport_unsupported",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let effective_headers = input.effective_headers(&parts.headers);
|
let effective_headers = input.effective_headers(&parts.headers);
|
||||||
|
|
||||||
let prepared_candidate = match prepare_header_authenticated_candidate(
|
let prepared_candidate = match prepare_header_authenticated_candidate(
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ use std::sync::Arc;
|
|||||||
use aether_contracts::ResolvedTransportProfile;
|
use aether_contracts::ResolvedTransportProfile;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::ai_serving::planner::antigravity::{
|
||||||
|
build_antigravity_v1internal_provider_request, AntigravityV1InternalRequestError,
|
||||||
|
AntigravityV1InternalRequestInput, ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
|
||||||
|
};
|
||||||
use crate::ai_serving::planner::candidate_preparation::{
|
use crate::ai_serving::planner::candidate_preparation::{
|
||||||
prepare_header_authenticated_candidate, prepare_header_authenticated_candidate_from_auth,
|
prepare_header_authenticated_candidate, prepare_header_authenticated_candidate_from_auth,
|
||||||
OauthPreparationContext,
|
OauthPreparationContext,
|
||||||
@@ -26,6 +30,7 @@ use crate::ai_serving::planner::standard::{
|
|||||||
openai_provider_request_contract_failure_extra_data, openai_responses_reasoning_replay_policy,
|
openai_provider_request_contract_failure_extra_data, openai_responses_reasoning_replay_policy,
|
||||||
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
request_body_build_failure_extra_data, request_conversion_failure_extra_data,
|
||||||
};
|
};
|
||||||
|
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
|
||||||
use crate::ai_serving::transport::kiro::{
|
use crate::ai_serving::transport::kiro::{
|
||||||
build_kiro_provider_headers, build_kiro_provider_request_body,
|
build_kiro_provider_headers, build_kiro_provider_request_body,
|
||||||
is_kiro_claude_messages_transport, KiroProviderHeadersInput, KiroRequestAuth,
|
is_kiro_claude_messages_transport, KiroProviderHeadersInput, KiroRequestAuth,
|
||||||
@@ -837,6 +842,29 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
|
|||||||
.await);
|
.await);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if normalized_provider_api_format == "gemini:generate_content"
|
||||||
|
&& is_antigravity_provider_transport(transport)
|
||||||
|
{
|
||||||
|
return Ok(build_antigravity_cross_format_payload_parts(
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
trace_id,
|
||||||
|
body_json,
|
||||||
|
input,
|
||||||
|
attempt,
|
||||||
|
transport,
|
||||||
|
spec_metadata.api_format,
|
||||||
|
provider_api_format,
|
||||||
|
prepared_candidate.mapped_model,
|
||||||
|
prepared_candidate.auth_header,
|
||||||
|
prepared_candidate.auth_value,
|
||||||
|
provider_request_body,
|
||||||
|
upstream_is_stream,
|
||||||
|
redaction.redacted,
|
||||||
|
)
|
||||||
|
.await);
|
||||||
|
}
|
||||||
|
|
||||||
if normalized_provider_api_format == "gemini:generate_content"
|
if normalized_provider_api_format == "gemini:generate_content"
|
||||||
&& is_gemini_cli_provider_transport(transport)
|
&& is_gemini_cli_provider_transport(transport)
|
||||||
{
|
{
|
||||||
@@ -963,6 +991,145 @@ fn apply_transport_request_body_semantics(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn build_antigravity_cross_format_payload_parts(
|
||||||
|
state: &AppState,
|
||||||
|
parts: &http::request::Parts,
|
||||||
|
trace_id: &str,
|
||||||
|
original_body_json: &serde_json::Value,
|
||||||
|
input: &LocalStandardDecisionInput,
|
||||||
|
attempt: &LocalStandardCandidateAttempt,
|
||||||
|
transport: &Arc<GatewayProviderTransportSnapshot>,
|
||||||
|
client_api_format: &str,
|
||||||
|
provider_api_format: &str,
|
||||||
|
mapped_model: String,
|
||||||
|
auth_header: String,
|
||||||
|
auth_value: String,
|
||||||
|
gemini_request_body: Value,
|
||||||
|
upstream_is_stream: bool,
|
||||||
|
request_redacted: bool,
|
||||||
|
) -> Option<LocalStandardCandidatePayloadParts> {
|
||||||
|
let candidate = &attempt.eligible.candidate;
|
||||||
|
let effective_headers = input.effective_headers(&parts.headers);
|
||||||
|
let resolved =
|
||||||
|
match build_antigravity_v1internal_provider_request(AntigravityV1InternalRequestInput {
|
||||||
|
state,
|
||||||
|
parts,
|
||||||
|
transport,
|
||||||
|
trace_id,
|
||||||
|
mapped_model: &mapped_model,
|
||||||
|
provider_api_format,
|
||||||
|
auth_header: &auth_header,
|
||||||
|
auth_value: &auth_value,
|
||||||
|
request_headers: effective_headers,
|
||||||
|
original_request_body: original_body_json,
|
||||||
|
gemini_request_body: &gemini_request_body,
|
||||||
|
upstream_is_stream,
|
||||||
|
same_format: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resolved) => resolved,
|
||||||
|
Err(AntigravityV1InternalRequestError::TransportUnsupported) => {
|
||||||
|
mark_skipped_local_standard_candidate(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"transport_unsupported",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(AntigravityV1InternalRequestError::EnvelopeUnsupported) => {
|
||||||
|
mark_skipped_local_standard_candidate_with_extra_data(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"provider_request_body_build_failed",
|
||||||
|
request_body_build_failure_extra_data(
|
||||||
|
original_body_json,
|
||||||
|
client_api_format,
|
||||||
|
provider_api_format,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(AntigravityV1InternalRequestError::UpstreamUrlUnavailable) => {
|
||||||
|
mark_skipped_local_standard_candidate_with_failure_diagnostic(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"upstream_url_missing",
|
||||||
|
CandidateFailureDiagnostic::upstream_url_missing(
|
||||||
|
client_api_format,
|
||||||
|
provider_api_format,
|
||||||
|
"standard_family_antigravity_url",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(AntigravityV1InternalRequestError::HeaderRulesApplyFailed) => {
|
||||||
|
mark_skipped_local_standard_candidate_with_failure_diagnostic(
|
||||||
|
state,
|
||||||
|
input,
|
||||||
|
trace_id,
|
||||||
|
candidate,
|
||||||
|
attempt.candidate_index,
|
||||||
|
&attempt.candidate_id,
|
||||||
|
"transport_header_rules_apply_failed",
|
||||||
|
CandidateFailureDiagnostic::header_rules_apply_failed(
|
||||||
|
client_api_format,
|
||||||
|
provider_api_format,
|
||||||
|
"standard_family_antigravity_headers",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut provider_request_headers = resolved.headers.headers;
|
||||||
|
apply_codex_openai_special_headers(
|
||||||
|
&mut provider_request_headers,
|
||||||
|
&resolved.body,
|
||||||
|
effective_headers,
|
||||||
|
resolved.transport.provider.provider_type.as_str(),
|
||||||
|
provider_api_format,
|
||||||
|
Some(trace_id),
|
||||||
|
resolved.transport.key.decrypted_auth_config.as_deref(),
|
||||||
|
);
|
||||||
|
request_identity_response_encoding_when_redacted(
|
||||||
|
&mut provider_request_headers,
|
||||||
|
request_redacted,
|
||||||
|
);
|
||||||
|
|
||||||
|
Some(LocalStandardCandidatePayloadParts {
|
||||||
|
auth_header: resolved.headers.auth_header,
|
||||||
|
auth_value: resolved.headers.auth_value,
|
||||||
|
mapped_model,
|
||||||
|
provider_api_format: provider_api_format.to_string(),
|
||||||
|
provider_request_body: resolved.body,
|
||||||
|
provider_request_headers,
|
||||||
|
upstream_url: resolved.upstream_url,
|
||||||
|
upstream_is_stream,
|
||||||
|
envelope_name: Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
|
||||||
|
transport: resolved.transport,
|
||||||
|
transport_profile: None,
|
||||||
|
request_redacted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn build_gemini_cli_cross_format_payload_parts(
|
async fn build_gemini_cli_cross_format_payload_parts(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
|
|||||||
@@ -2893,6 +2893,22 @@ fn ai_serving_standard_attempts_consume_eligible_local_candidates_without_transp
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let standard_family_request = read_workspace_file(
|
||||||
|
"apps/aether-gateway/src/ai_serving/planner/standard/family/request.rs",
|
||||||
|
);
|
||||||
|
for pattern in [
|
||||||
|
"is_antigravity_provider_transport(",
|
||||||
|
"build_antigravity_v1internal_provider_request(",
|
||||||
|
"is_gemini_cli_provider_transport(",
|
||||||
|
"build_gemini_cli_v1internal_provider_request(",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
standard_family_request.contains(pattern),
|
||||||
|
"standard family request preparation should build v1internal envelopes through {pattern} \
|
||||||
|
so a cross-format client never posts a bare Gemini body to a v1internal URL"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let provider_transport_standard =
|
let provider_transport_standard =
|
||||||
read_workspace_file("crates/aether-provider/transport/src/standard/mod.rs");
|
read_workspace_file("crates/aether-provider/transport/src/standard/mod.rs");
|
||||||
for pattern in [
|
for pattern in [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||||
|
use super::super::transport_proxy_is_locally_supported;
|
||||||
use super::auth::{
|
use super::auth::{
|
||||||
resolve_local_antigravity_request_auth, AntigravityRequestAuth, AntigravityRequestAuthSupport,
|
resolve_local_antigravity_request_auth, AntigravityRequestAuth, AntigravityRequestAuthSupport,
|
||||||
AntigravityRequestAuthUnsupportedReason, ANTIGRAVITY_PROVIDER_TYPE,
|
AntigravityRequestAuthUnsupportedReason, ANTIGRAVITY_PROVIDER_TYPE,
|
||||||
@@ -87,11 +88,11 @@ pub fn classify_local_antigravity_request_support(
|
|||||||
AntigravityRequestSideUnsupportedReason::UnsupportedBodyRules,
|
AntigravityRequestSideUnsupportedReason::UnsupportedBodyRules,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if transport.provider.proxy.is_some()
|
// A configured proxy is carried by the execution plan itself, so it only
|
||||||
|| transport.endpoint.proxy.is_some()
|
// disqualifies the local request when it cannot be resolved into a usable
|
||||||
|| transport.key.proxy.is_some()
|
// snapshot. Transport profiles stay unsupported because the v1internal
|
||||||
|| transport.key.fingerprint.is_some()
|
// payload never carries one.
|
||||||
{
|
if !transport_proxy_is_locally_supported(transport) || transport.key.fingerprint.is_some() {
|
||||||
return AntigravityRequestSideSupport::Unsupported(
|
return AntigravityRequestSideSupport::Unsupported(
|
||||||
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig,
|
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig,
|
||||||
);
|
);
|
||||||
@@ -114,3 +115,145 @@ pub fn classify_local_antigravity_request_support(
|
|||||||
|
|
||||||
AntigravityRequestSideSupport::Supported(AntigravityRequestSideSpec { auth, request_type })
|
AntigravityRequestSideSupport::Supported(AntigravityRequestSideSpec { auth, request_type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::super::request::AntigravityEnvelopeRequestType;
|
||||||
|
use super::{
|
||||||
|
classify_local_antigravity_request_support, AntigravityRequestSideSupport,
|
||||||
|
AntigravityRequestSideUnsupportedReason,
|
||||||
|
};
|
||||||
|
use crate::snapshot::{
|
||||||
|
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||||
|
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||||
|
GatewayProviderTransportSnapshot {
|
||||||
|
provider: GatewayProviderTransportProvider {
|
||||||
|
id: "provider-1".to_string(),
|
||||||
|
name: "Antigravity".to_string(),
|
||||||
|
provider_type: "antigravity".to_string(),
|
||||||
|
website: None,
|
||||||
|
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-1".to_string(),
|
||||||
|
provider_id: "provider-1".to_string(),
|
||||||
|
api_format: "gemini:generate_content".to_string(),
|
||||||
|
api_family: Some("gemini".to_string()),
|
||||||
|
endpoint_kind: Some("generate_content".to_string()),
|
||||||
|
is_active: true,
|
||||||
|
base_url: "https://daily-cloudcode-pa.googleapis.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: "oauth".to_string(),
|
||||||
|
is_active: true,
|
||||||
|
api_formats: Some(vec!["gemini:generate_content".to_string()]),
|
||||||
|
auth_type_by_format: None,
|
||||||
|
allow_auth_channel_mismatch_formats: None,
|
||||||
|
allowed_models: None,
|
||||||
|
capabilities: None,
|
||||||
|
rate_multipliers: None,
|
||||||
|
global_priority_by_format: None,
|
||||||
|
expires_at_unix_secs: None,
|
||||||
|
proxy: None,
|
||||||
|
fingerprint: None,
|
||||||
|
upstream_metadata: None,
|
||||||
|
decrypted_api_key: "__placeholder__".to_string(),
|
||||||
|
decrypted_auth_config: Some(
|
||||||
|
r#"{"provider_type":"antigravity","refresh_token":"rt","cloudaicompanionProject":"project-1"}"#
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn classify(transport: &GatewayProviderTransportSnapshot) -> AntigravityRequestSideSupport {
|
||||||
|
classify_local_antigravity_request_support(
|
||||||
|
transport,
|
||||||
|
&json!({"contents": []}),
|
||||||
|
AntigravityEnvelopeRequestType::Agent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_unsupported_network_config(support: AntigravityRequestSideSupport) {
|
||||||
|
assert_eq!(
|
||||||
|
support,
|
||||||
|
AntigravityRequestSideSupport::Unsupported(
|
||||||
|
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resolvable_tunnel_node_proxy_keeps_the_envelope_supported() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.provider.proxy = Some(json!({
|
||||||
|
"enabled": true,
|
||||||
|
"node_id": "702d158b-a432-4694-94cc-3bec13dbbc20",
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
classify(&transport),
|
||||||
|
AntigravityRequestSideSupport::Supported(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_resolvable_url_proxy_keeps_the_envelope_supported() {
|
||||||
|
for proxy_owner in ["provider", "endpoint", "key"] {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
let proxy = Some(json!({"enabled": true, "url": "http://127.0.0.1:17000"}));
|
||||||
|
match proxy_owner {
|
||||||
|
"provider" => transport.provider.proxy = proxy,
|
||||||
|
"endpoint" => transport.endpoint.proxy = proxy,
|
||||||
|
_ => transport.key.proxy = proxy,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
classify(&transport),
|
||||||
|
AntigravityRequestSideSupport::Supported(_)
|
||||||
|
),
|
||||||
|
"a {proxy_owner} proxy should not disqualify the antigravity envelope"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_proxy_without_a_route_still_disqualifies_the_envelope() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.provider.proxy = Some(json!({"enabled": true}));
|
||||||
|
|
||||||
|
assert_unsupported_network_config(classify(&transport));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_key_fingerprint_still_disqualifies_the_envelope() {
|
||||||
|
let mut transport = sample_transport();
|
||||||
|
transport.key.fingerprint = Some(json!({"transport_profile": "chrome"}));
|
||||||
|
|
||||||
|
assert_unsupported_network_config(classify(&transport));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user