支持 Gemini Interactions 与 Antigravity 配额精细化

This commit is contained in:
MMEXA
2026-07-08 22:34:29 +08:00
parent 1ca83ca3fb
commit 80a6579766
41 changed files with 1072 additions and 225 deletions
@@ -150,6 +150,9 @@ fn build_transport_request_url_inner(
params.request_query,
gemini_embedding_batch,
),
"gemini:interactions" => {
build_gemini_interactions_url(&transport.endpoint.base_url, params.request_query)
}
"doubao:embedding" => build_passthrough_path_url(
&transport.endpoint.base_url,
"/embeddings",
@@ -345,7 +348,9 @@ fn build_transport_hook_url(
_ => {}
}
if is_antigravity_provider_transport(transport) {
if is_antigravity_provider_transport(transport)
&& normalized_provider_api_format == "gemini:generate_content"
{
let query = params.request_query.map(|raw| {
form_urlencoded::parse(raw.as_bytes())
.into_owned()
@@ -398,7 +403,8 @@ fn build_path_params(
}
let provider_api_format =
aether_ai_formats::normalize_api_format_alias(params.provider_api_format);
if provider_api_format.starts_with("gemini:") {
if provider_api_format == "gemini:generate_content" || provider_api_format == "gemini:embedding"
{
path_params.insert(
"action",
if provider_api_format == "gemini:embedding" {
@@ -445,6 +451,10 @@ fn build_provider_rerank_v1_url(upstream_base_url: &str, query: Option<&str>) ->
build_provider_api_root_url(upstream_base_url, "/rerank", query)
}
fn build_gemini_interactions_url(upstream_base_url: &str, query: Option<&str>) -> Option<String> {
build_passthrough_path_url(upstream_base_url, "/v1/interactions", query, &["key"])
}
fn build_provider_api_root_url(
upstream_base_url: &str,
path: &str,
@@ -1116,6 +1126,76 @@ mod tests {
);
}
#[test]
fn gemini_interactions_request_url_uses_stable_v1_endpoint() {
let gemini = sample_transport(
"gemini",
"gemini:interactions",
"https://generativelanguage.googleapis.com",
None,
);
let versioned = sample_transport(
"gemini",
"gemini:interactions",
"https://generativelanguage.googleapis.com/v1",
None,
);
assert_eq!(
build_transport_request_url(
&gemini,
TransportRequestUrlParams {
provider_api_format: "gemini:interactions",
mapped_model: Some("gemini-3.5-flash"),
upstream_is_stream: false,
request_query: Some("key=client-key&trace=1"),
kiro_api_region: None,
},
)
.as_deref(),
Some("https://generativelanguage.googleapis.com/v1/interactions?trace=1")
);
assert_eq!(
build_transport_request_url(
&versioned,
TransportRequestUrlParams {
provider_api_format: "gemini:interactions",
mapped_model: Some("gemini-3.5-flash"),
upstream_is_stream: true,
request_query: Some("key=client-key&trace=2"),
kiro_api_region: None,
},
)
.as_deref(),
Some("https://generativelanguage.googleapis.com/v1/interactions?trace=2")
);
}
#[test]
fn antigravity_hook_is_limited_to_generate_content() {
let transport = sample_transport(
"antigravity",
"gemini:interactions",
"https://daily-cloudcode-pa.googleapis.com",
None,
);
assert_eq!(
build_transport_request_url(
&transport,
TransportRequestUrlParams {
provider_api_format: "gemini:interactions",
mapped_model: Some("gemini-3.5-flash"),
upstream_is_stream: false,
request_query: Some("key=client-key"),
kiro_api_region: None,
},
)
.as_deref(),
Some("https://daily-cloudcode-pa.googleapis.com/v1/interactions")
);
}
#[test]
fn embedding_request_url_preserves_google_openai_compat_roots() {
let developer_api_openai = sample_transport(
@@ -265,13 +265,39 @@ fn build_same_format_provider_request_body_inner(
}
}
SameFormatProviderFamily::Gemini => {
if provider_request_body.remove("model").is_some() {
record_compatibility_edit(
&mut compatibility_edits,
"model",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"removed top-level model because Gemini model is carried by the upstream URL",
if aether_ai_formats::api_format_alias_matches(
input.provider_api_format,
"gemini:interactions",
) {
let rewrite_field = if provider_request_body.contains_key("model") {
"model"
} else if provider_request_body.contains_key("agent") {
"agent"
} else {
"model"
};
let previous_value = provider_request_body.get(rewrite_field).cloned();
provider_request_body.insert(
rewrite_field.to_string(),
Value::String(input.mapped_model.to_string()),
);
if previous_value != Some(Value::String(input.mapped_model.to_string())) {
record_compatibility_edit(
&mut compatibility_edits,
rewrite_field,
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"rewrote Gemini Interactions routing field to mapped upstream target",
);
}
} else {
if provider_request_body.remove("model").is_some() {
record_compatibility_edit(
&mut compatibility_edits,
"model",
SameFormatProviderCompatibilityEditAction::RuntimeRewrite,
"removed top-level model because Gemini model is carried by the upstream URL",
);
}
}
}
}
@@ -577,6 +603,7 @@ pub fn same_format_provider_transport_unsupported_reason_for_trace(
"openai:responses:compact" => "openai:responses:compact",
"claude:messages" => "claude:messages",
"gemini:generate_content" => "gemini:generate_content",
"gemini:interactions" => "gemini:interactions",
_ => return Some("transport_api_format_unsupported"),
};
let behavior = classify_same_format_provider_request_behavior(
@@ -1210,6 +1237,62 @@ mod tests {
assert!(body.get("stream").is_none());
}
#[test]
fn same_format_gemini_interactions_body_keeps_body_routing_target() {
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &json!({
"model": "client-model",
"input": [{"role": "user", "content": "hello"}],
"stream": true
}),
mapped_model: "gemini-3.5-flash",
client_api_format: "gemini:interactions",
provider_api_format: "gemini:interactions",
source_model: Some("client-model"),
family: SameFormatProviderFamily::Gemini,
body_rules: None,
request_headers: None,
upstream_is_stream: true,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
})
.expect("body should build");
assert_eq!(body.get("model"), Some(&json!("gemini-3.5-flash")));
assert_eq!(body.get("stream"), Some(&json!(true)));
}
#[test]
fn same_format_gemini_interactions_body_rewrites_agent_target() {
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
body_json: &json!({
"agent": "antigravity-preview-05-2026",
"input": "hello"
}),
mapped_model: "antigravity-preview-05-2026",
client_api_format: "gemini:interactions",
provider_api_format: "gemini:interactions",
source_model: None,
family: SameFormatProviderFamily::Gemini,
body_rules: None,
request_headers: None,
upstream_is_stream: false,
force_body_stream_field: false,
kiro_auth_config: None,
is_claude_code: false,
enable_model_directives: false,
})
.expect("body should build");
assert_eq!(
body.get("agent"),
Some(&json!("antigravity-preview-05-2026"))
);
assert!(body.get("model").is_none());
}
#[test]
fn same_format_gemini_body_strips_function_response_id_for_upstream() {
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {