mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
Add Antigravity v1internal gateway adapter
This commit is contained in:
@@ -186,6 +186,9 @@ fn select_primary_credential(
|
||||
if signature.starts_with("gemini:") {
|
||||
return select_gemini_credential(bundle);
|
||||
}
|
||||
if signature.starts_with("antigravity:") {
|
||||
return select_antigravity_credential(bundle);
|
||||
}
|
||||
if signature.starts_with("claude:") {
|
||||
return select_claude_messages_credential(bundle);
|
||||
}
|
||||
@@ -196,6 +199,20 @@ fn select_primary_credential(
|
||||
select_generic_credential(bundle)
|
||||
}
|
||||
|
||||
fn select_antigravity_credential(
|
||||
bundle: &GatewayCredentialBundle,
|
||||
) -> Option<GatewayPrimaryCredential> {
|
||||
first_provider_api_key(
|
||||
bundle,
|
||||
&[
|
||||
GatewayCredentialCarrier::XApiKey,
|
||||
GatewayCredentialCarrier::ApiKey,
|
||||
],
|
||||
)
|
||||
.or_else(|| first_bearer_token(bundle))
|
||||
.or_else(|| select_cookie_credential(bundle))
|
||||
}
|
||||
|
||||
fn select_openai_credential(bundle: &GatewayCredentialBundle) -> Option<GatewayPrimaryCredential> {
|
||||
first_provider_api_key(
|
||||
bundle,
|
||||
@@ -459,6 +476,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_antigravity_aether_api_key_over_google_bearer() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
"Bearer google-oauth-access-token".parse().unwrap(),
|
||||
);
|
||||
headers.insert("x-api-key", "sk-aether-antigravity".parse().unwrap());
|
||||
|
||||
let extracted = extract_request_credentials(
|
||||
&headers,
|
||||
&uri("/v1internal:streamGenerateContent?alt=sse"),
|
||||
"antigravity:v1internal",
|
||||
);
|
||||
assert_eq!(
|
||||
extracted.primary,
|
||||
Some(GatewayPrimaryCredential::ProviderApiKey {
|
||||
raw: "sk-aether-antigravity".to_string(),
|
||||
carrier: GatewayCredentialCarrier::XApiKey,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_gemini_query_key_over_header_key() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
|
||||
@@ -712,7 +712,12 @@ async fn build_data_backed_auth_context(
|
||||
})
|
||||
} else if snapshot
|
||||
.effective_allowed_api_formats()
|
||||
.is_some_and(|allowed| !contains_api_format_or_alias(allowed, auth_endpoint_signature))
|
||||
.is_some_and(|allowed| {
|
||||
!contains_api_format_or_alias(
|
||||
allowed,
|
||||
auth_gate_api_format(auth_endpoint_signature).as_str(),
|
||||
)
|
||||
})
|
||||
{
|
||||
Some(GatewayLocalAuthRejection::ApiFormatNotAllowed {
|
||||
api_format: auth_endpoint_signature.to_string(),
|
||||
@@ -747,6 +752,15 @@ fn normalize_api_format_alias(value: &str) -> String {
|
||||
crate::ai_serving::normalize_api_format_alias(value)
|
||||
}
|
||||
|
||||
fn auth_gate_api_format(auth_endpoint_signature: &str) -> String {
|
||||
let normalized = normalize_api_format_alias(auth_endpoint_signature);
|
||||
if normalized == "antigravity:v1internal" {
|
||||
"gemini:generate_content".to_string()
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn api_format_matches(left: &str, right: &str) -> bool {
|
||||
aether_scheduler_core::api_format_matches_allowed_value(left, right)
|
||||
}
|
||||
@@ -1261,6 +1275,58 @@ mod tests {
|
||||
assert_eq!(auth_context.local_rejection, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn data_backed_auth_context_allows_antigravity_v1internal_for_gemini_generate_content_keys(
|
||||
) {
|
||||
let api_key = "sk-test-antigravity-v1internal";
|
||||
let mut snapshot = sample_snapshot("key-ant-v1internal", "user-ant-v1internal");
|
||||
snapshot.user_allowed_providers = Some(vec!["antigravity".to_string()]);
|
||||
snapshot.api_key_allowed_providers = Some(vec!["antigravity".to_string()]);
|
||||
snapshot.user_allowed_api_formats = Some(vec!["gemini:generate_content".to_string()]);
|
||||
snapshot.api_key_allowed_api_formats = Some(vec!["gemini:generate_content".to_string()]);
|
||||
let repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some(hash_api_key(api_key)),
|
||||
snapshot,
|
||||
)]));
|
||||
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider(
|
||||
"provider-antigravity-1",
|
||||
"Antigravity",
|
||||
"antigravity",
|
||||
)],
|
||||
vec![sample_endpoint(
|
||||
"endpoint-antigravity-1",
|
||||
"provider-antigravity-1",
|
||||
"gemini:generate_content",
|
||||
)],
|
||||
Vec::new(),
|
||||
));
|
||||
let data = GatewayDataState::with_auth_api_key_reader_for_tests(repository)
|
||||
.with_provider_catalog_reader(provider_catalog);
|
||||
let state = AppState::new()
|
||||
.expect("state should build")
|
||||
.with_data_state_for_tests(data);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-api-key", api_key.parse().unwrap());
|
||||
headers.insert(
|
||||
http::header::AUTHORIZATION,
|
||||
"Bearer google-oauth-access-token".parse().unwrap(),
|
||||
);
|
||||
|
||||
let auth_context = resolve_data_backed_auth_context(
|
||||
&state,
|
||||
&headers,
|
||||
&uri("/v1internal:streamGenerateContent?alt=sse"),
|
||||
Some("antigravity:v1internal"),
|
||||
)
|
||||
.await
|
||||
.expect("resolution should succeed")
|
||||
.expect("auth context should exist");
|
||||
|
||||
assert_eq!(auth_context.local_rejection, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn data_backed_auth_context_allows_provider_id_for_convertible_endpoint_format() {
|
||||
let api_key = "sk-test-provider-convertible-endpoint";
|
||||
|
||||
@@ -8,7 +8,9 @@ pub(super) fn classify_ai_public_route(
|
||||
normalized_path: &str,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Option<ClassifiedRoute> {
|
||||
if method == http::Method::POST && normalized_path == "/v1/chat/completions" {
|
||||
if let Some(route) = classify_antigravity_v1internal_route(method, normalized_path) {
|
||||
Some(route)
|
||||
} else if method == http::Method::POST && normalized_path == "/v1/chat/completions" {
|
||||
Some(classified(
|
||||
"ai_public",
|
||||
"openai",
|
||||
@@ -156,3 +158,34 @@ pub(super) fn classify_ai_public_route(
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_antigravity_v1internal_route(
|
||||
method: &http::Method,
|
||||
normalized_path: &str,
|
||||
) -> Option<ClassifiedRoute> {
|
||||
if method != http::Method::POST {
|
||||
return None;
|
||||
}
|
||||
|
||||
let action = normalized_path.strip_prefix("/v1internal:")?;
|
||||
let (route_kind, execution_runtime_candidate) = match action {
|
||||
"loadCodeAssist" => ("load_code_assist", false),
|
||||
"fetchAvailableModels" => ("fetch_available_models", false),
|
||||
"fetchUserInfo" => ("fetch_user_info", false),
|
||||
"fetchAdminControls" => ("fetch_admin_controls", false),
|
||||
"setUserSettings" => ("set_user_settings", false),
|
||||
"listExperiments" => ("list_experiments", false),
|
||||
"recordCodeAssistMetrics" => ("record_code_assist_metrics", false),
|
||||
"streamGenerateContent" => ("stream_generate_content", true),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(classified_with_request_auth_channel(
|
||||
"ai_public",
|
||||
"antigravity",
|
||||
route_kind,
|
||||
"bearer_like",
|
||||
"antigravity:v1internal",
|
||||
execution_runtime_candidate,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -274,3 +274,86 @@ fn classifies_gemini_predict_long_running_as_video_route() {
|
||||
);
|
||||
assert!(decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_antigravity_v1internal_control_plane_routes() {
|
||||
let headers = headers(&[
|
||||
("authorization", "Bearer ant-access-token"),
|
||||
("user-agent", "antigravity/cli/1.0.2 linux/arm64"),
|
||||
]);
|
||||
|
||||
for (path, route_kind) in [
|
||||
("/v1internal:loadCodeAssist", "load_code_assist"),
|
||||
("/v1internal:fetchAvailableModels", "fetch_available_models"),
|
||||
("/v1internal:fetchUserInfo", "fetch_user_info"),
|
||||
("/v1internal:fetchAdminControls", "fetch_admin_controls"),
|
||||
("/v1internal:setUserSettings", "set_user_settings"),
|
||||
("/v1internal:listExperiments", "list_experiments"),
|
||||
(
|
||||
"/v1internal:recordCodeAssistMetrics",
|
||||
"record_code_assist_metrics",
|
||||
),
|
||||
] {
|
||||
let uri: Uri = path.parse().expect("uri should parse");
|
||||
let decision = classify_control_route(&http::Method::POST, &uri, &headers)
|
||||
.expect("route should classify");
|
||||
|
||||
assert_eq!(decision.route_class.as_deref(), Some("ai_public"));
|
||||
assert_eq!(decision.route_family.as_deref(), Some("antigravity"));
|
||||
assert_eq!(decision.route_kind.as_deref(), Some(route_kind));
|
||||
assert_eq!(
|
||||
decision.request_auth_channel.as_deref(),
|
||||
Some("bearer_like")
|
||||
);
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("antigravity:v1internal")
|
||||
);
|
||||
assert!(
|
||||
!decision.is_execution_runtime_candidate(),
|
||||
"control-plane route {path} must be handled by local facade before execution runtime"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_antigravity_stream_generate_content_as_execution_route() {
|
||||
let headers = headers(&[
|
||||
("authorization", "Bearer ant-access-token"),
|
||||
("user-agent", "antigravity/cli/1.0.2 linux/arm64"),
|
||||
]);
|
||||
let uri: Uri = "/v1internal:streamGenerateContent?alt=sse"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.route_class.as_deref(), Some("ai_public"));
|
||||
assert_eq!(decision.route_family.as_deref(), Some("antigravity"));
|
||||
assert_eq!(
|
||||
decision.route_kind.as_deref(),
|
||||
Some("stream_generate_content")
|
||||
);
|
||||
assert_eq!(
|
||||
decision.request_auth_channel.as_deref(),
|
||||
Some("bearer_like")
|
||||
);
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("antigravity:v1internal")
|
||||
);
|
||||
assert!(decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_antigravity_v1internal_route() {
|
||||
let headers = headers(&[
|
||||
("authorization", "Bearer ant-access-token"),
|
||||
("user-agent", "antigravity/cli/1.0.2 linux/arm64"),
|
||||
]);
|
||||
let uri: Uri = "/v1internal:deleteEverything"
|
||||
.parse()
|
||||
.expect("uri should parse");
|
||||
|
||||
assert!(classify_control_route(&http::Method::POST, &uri, &headers).is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user