Merge branch 'fawney19:main' into main

This commit is contained in:
Start
2026-07-09 15:11:44 +08:00
committed by GitHub
67 changed files with 3107 additions and 461 deletions
@@ -0,0 +1,169 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use serde_json::Value;
use crate::ai_serving::transport::antigravity::{
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestAuth, AntigravityRequestAuthUnsupportedReason,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
AntigravityRequestSideUnsupportedReason,
};
use crate::ai_serving::transport::{
build_standard_provider_request_headers, GatewayProviderTransportSnapshot,
StandardProviderRequestHeaders, StandardProviderRequestHeadersInput,
};
use crate::AppState;
pub(crate) const ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME: &str = "antigravity:v1internal";
pub(crate) enum AntigravityV1InternalRequestError {
TransportUnsupported,
EnvelopeUnsupported,
UpstreamUrlUnavailable,
HeaderRulesApplyFailed,
}
pub(crate) struct AntigravityV1InternalRequestInput<'a> {
pub(crate) state: &'a AppState,
pub(crate) parts: &'a http::request::Parts,
pub(crate) transport: &'a Arc<GatewayProviderTransportSnapshot>,
pub(crate) trace_id: &'a str,
pub(crate) mapped_model: &'a str,
pub(crate) provider_api_format: &'a str,
pub(crate) auth_header: &'a str,
pub(crate) auth_value: &'a str,
pub(crate) request_headers: &'a http::HeaderMap,
pub(crate) original_request_body: &'a Value,
pub(crate) gemini_request_body: &'a Value,
pub(crate) upstream_is_stream: bool,
pub(crate) same_format: bool,
}
pub(crate) struct AntigravityV1InternalRequest {
pub(crate) transport: Arc<GatewayProviderTransportSnapshot>,
pub(crate) body: Value,
pub(crate) headers: StandardProviderRequestHeaders,
pub(crate) upstream_url: String,
}
pub(crate) async fn build_antigravity_v1internal_provider_request(
input: AntigravityV1InternalRequestInput<'_>,
) -> Result<AntigravityV1InternalRequest, AntigravityV1InternalRequestError> {
let payload = build_antigravity_v1internal_payload(
input.state,
input.transport,
input.trace_id,
input.mapped_model,
input.gemini_request_body,
)
.await?;
let upstream_url = crate::ai_serving::build_provider_transport_request_url_for_request_body(
&payload.transport,
input.provider_api_format,
Some(input.mapped_model),
input.upstream_is_stream,
input.parts.uri.query(),
None,
Some(&payload.body),
)
.ok_or(AntigravityV1InternalRequestError::UpstreamUrlUnavailable)?;
let extra_headers: BTreeMap<String, String> =
build_antigravity_static_identity_headers(&payload.auth);
let mut headers =
build_standard_provider_request_headers(StandardProviderRequestHeadersInput {
transport: &payload.transport,
provider_api_format: input.provider_api_format,
same_format: input.same_format,
headers: input.request_headers,
auth_header: input.auth_header,
auth_value: input.auth_value,
extra_headers: &extra_headers,
header_rules: payload.transport.endpoint.header_rules.as_ref(),
provider_request_body: &payload.body,
original_request_body: input.original_request_body,
upstream_is_stream: input.upstream_is_stream,
})
.ok_or(AntigravityV1InternalRequestError::HeaderRulesApplyFailed)?;
headers
.headers
.insert("accept".to_string(), "text/event-stream".to_string());
Ok(AntigravityV1InternalRequest {
transport: payload.transport,
body: payload.body,
headers,
upstream_url,
})
}
struct AntigravityV1InternalPayload {
transport: Arc<GatewayProviderTransportSnapshot>,
auth: AntigravityRequestAuth,
body: Value,
}
async fn build_antigravity_v1internal_payload(
state: &AppState,
transport: &Arc<GatewayProviderTransportSnapshot>,
trace_id: &str,
mapped_model: &str,
gemini_request_body: &Value,
) -> Result<AntigravityV1InternalPayload, AntigravityV1InternalRequestError> {
let mut resolved_transport = Arc::clone(transport);
let mut antigravity_support = classify_local_antigravity_request_support(
&resolved_transport,
gemini_request_body,
AntigravityEnvelopeRequestType::Agent,
);
if matches!(
antigravity_support,
AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::MissingProjectId
)
)
) {
if let Some(hydrated) = state
.hydrate_antigravity_project_metadata_for_transport(&resolved_transport)
.await
{
resolved_transport = Arc::new(hydrated);
antigravity_support = classify_local_antigravity_request_support(
&resolved_transport,
gemini_request_body,
AntigravityEnvelopeRequestType::Agent,
);
}
}
let auth = match antigravity_support {
AntigravityRequestSideSupport::Supported(spec) => spec.auth,
AntigravityRequestSideSupport::Unsupported(_) => {
return Err(AntigravityV1InternalRequestError::TransportUnsupported);
}
};
let body = match build_antigravity_safe_v1internal_request(
&auth,
trace_id,
mapped_model,
gemini_request_body,
AntigravityEnvelopeRequestType::Agent,
) {
AntigravityRequestEnvelopeSupport::Supported(envelope) => envelope,
AntigravityRequestEnvelopeSupport::Unsupported(_) => {
return Err(AntigravityV1InternalRequestError::EnvelopeUnsupported);
}
};
Ok(AntigravityV1InternalPayload {
transport: resolved_transport,
auth,
body,
})
}
@@ -1,6 +1,7 @@
use crate::ai_serving::{AiExecutionDecision, AiExecutionPlanPayload, GatewayControlDecision}; use crate::ai_serving::{AiExecutionDecision, AiExecutionPlanPayload, GatewayControlDecision};
use crate::{AppState, GatewayError}; use crate::{AppState, GatewayError};
mod antigravity;
mod candidate_affinity_cache; mod candidate_affinity_cache;
mod candidate_materialization; mod candidate_materialization;
mod candidate_metadata; mod candidate_metadata;
@@ -4,6 +4,10 @@ use std::sync::Arc;
use aether_contracts::ResolvedTransportProfile; use aether_contracts::ResolvedTransportProfile;
use serde_json::{json, Value}; use serde_json::{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,
@@ -27,6 +31,7 @@ use crate::ai_serving::planner::standard::{
build_local_openai_chat_upstream_url, request_body_build_failure_extra_data, build_local_openai_chat_upstream_url, request_body_build_failure_extra_data,
request_conversion_failure_extra_data, request_conversion_failure_extra_data,
}; };
use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth; use crate::ai_serving::transport::auth::resolve_local_openai_bearer_auth;
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,
@@ -183,6 +188,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
.provider_type .provider_type
.trim() .trim()
.eq_ignore_ascii_case("grok"); .eq_ignore_ascii_case("grok");
let is_antigravity = is_antigravity_provider_transport(transport);
let is_gemini_cli = is_gemini_cli_provider_transport(transport); let is_gemini_cli = is_gemini_cli_provider_transport(transport);
if is_grok && is_grok_text_provider_api_format(provider_api_format) { if is_grok && is_grok_text_provider_api_format(provider_api_format) {
@@ -549,7 +555,9 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
transport, transport,
conversion_kind, conversion_kind,
) { ) {
if !(is_gemini_cli && normalized_provider_api_format == "gemini:generate_content") { if !((is_antigravity || is_gemini_cli)
&& normalized_provider_api_format == "gemini:generate_content")
{
mark_skipped_local_openai_chat_candidate( mark_skipped_local_openai_chat_candidate(
state, state,
input, input,
@@ -757,6 +765,28 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
) )
.await); .await);
} }
if provider_api_format == "gemini:generate_content" && is_antigravity {
return Ok(build_antigravity_openai_chat_cross_format_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
decision_kind,
transport,
provider_api_format.as_str(),
prepared_candidate.mapped_model,
prepared_candidate.auth_header,
prepared_candidate.auth_value,
provider_request_body,
upstream_is_stream,
redaction.redacted,
)
.await);
}
let Some(upstream_url) = build_cross_format_openai_chat_upstream_url( let Some(upstream_url) = build_cross_format_openai_chat_upstream_url(
parts, parts,
@@ -857,6 +887,158 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
})) }))
} }
#[allow(clippy::too_many_arguments)]
async fn build_antigravity_openai_chat_cross_format_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
decision_kind: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
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<LocalOpenAiChatCandidatePayloadParts> {
let candidate = &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_openai_chat_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_unsupported",
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::EnvelopeUnsupported) => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
"openai:chat",
provider_api_format,
"openai_chat_antigravity_envelope",
),
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::UpstreamUrlUnavailable) => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
"openai:chat",
provider_api_format,
"openai_chat_antigravity_url",
),
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::HeaderRulesApplyFailed) => {
mark_skipped_local_openai_chat_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
"openai:chat",
provider_api_format,
"openai_chat_antigravity_headers",
),
)
.await;
return None;
}
};
let mut provider_request_headers = resolved.headers.headers;
apply_codex_openai_responses_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,
);
let resolved_report_kind = if decision_kind == OPENAI_CHAT_STREAM_PLAN_KIND {
"openai_chat_stream_success".to_string()
} else {
"openai_chat_sync_finalize".to_string()
};
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats("openai:chat", provider_api_format);
Some(LocalOpenAiChatCandidatePayloadParts {
client_api_format: "openai:chat".to_string(),
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,
execution_strategy,
conversion_mode,
report_kind: resolved_report_kind,
envelope_name: Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
transport: resolved.transport,
request_redacted,
transport_profile: None,
image_request_summary: None,
})
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn build_gemini_cli_openai_chat_cross_format_payload_parts( async fn build_gemini_cli_openai_chat_cross_format_payload_parts(
state: &AppState, state: &AppState,
@@ -1923,6 +2105,39 @@ mod tests {
} }
} }
fn sample_antigravity_transport() -> GatewayProviderTransportSnapshot {
let mut transport = sample_gemini_cli_transport();
transport.provider.name = "antigravity".to_string();
transport.provider.provider_type = "antigravity".to_string();
transport.endpoint.base_url = "https://antigravity.googleapis.com".to_string();
transport.endpoint.custom_path = None;
transport.key.auth_type = "oauth".to_string();
transport.key.decrypted_api_key = "__placeholder__".to_string();
transport.key.upstream_metadata = None;
transport.key.decrypted_auth_config = Some(
json!({
"provider_type": "antigravity",
"project_id": "test-antigravity-project",
"client_version": "1.2.3",
"session_id": "sess-antigravity-chat",
"access_token_import_temporary": true,
"headers": {
"Authorization": "Bearer imported-antigravity-token"
}
})
.to_string(),
);
transport
}
fn sample_antigravity_eligible() -> EligibleLocalExecutionCandidate {
let mut eligible = sample_gemini_cli_eligible();
eligible.candidate.provider_name = "antigravity".to_string();
eligible.candidate.provider_type = "antigravity".to_string();
eligible.transport = Arc::new(sample_antigravity_transport());
eligible
}
#[tokio::test] #[tokio::test]
async fn openai_chat_to_gemini_cli_wraps_cross_format_body_in_v1internal_envelope() { async fn openai_chat_to_gemini_cli_wraps_cross_format_body_in_v1internal_envelope() {
let state = AppState::new().expect("state should build"); let state = AppState::new().expect("state should build");
@@ -1989,6 +2204,96 @@ mod tests {
.is_some()); .is_some());
} }
#[tokio::test]
async fn openai_chat_to_antigravity_wraps_cross_format_body_in_v1internal_envelope() {
let state = AppState::new().expect("state should build");
let request = http::Request::builder()
.method("POST")
.uri("/v1/chat/completions")
.header(http::header::CONTENT_TYPE, "application/json")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let body_json = json!({
"model": "gemini-3.5-flash",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
});
let payload = resolve_local_openai_chat_candidate_payload_parts(
&state,
&parts,
"trace-openai-chat-antigravity",
&body_json,
&sample_input(),
None,
&sample_antigravity_eligible(),
0,
"candidate-0",
OPENAI_CHAT_STREAM_PLAN_KIND,
"openai_chat_stream_success",
true,
)
.await
.expect("candidate resolution should not fail")
.expect("antigravity candidate should build a payload");
assert_eq!(
payload.upstream_url,
"https://antigravity.googleapis.com/v1internal:streamGenerateContent?alt=sse"
);
assert_eq!(payload.envelope_name, Some("antigravity:v1internal"));
assert_eq!(
payload
.provider_request_headers
.get("authorization")
.map(String::as_str),
Some("Bearer imported-antigravity-token")
);
assert_eq!(
payload
.provider_request_headers
.get("x-client-name")
.map(String::as_str),
Some("antigravity")
);
assert_eq!(
payload
.provider_request_headers
.get("x-client-version")
.map(String::as_str),
Some("1.2.3")
);
assert_eq!(
payload
.provider_request_headers
.get("x-vscode-sessionid")
.map(String::as_str),
Some("sess-antigravity-chat")
);
assert_eq!(
payload.provider_request_body["project"],
"test-antigravity-project"
);
assert_eq!(
payload.provider_request_body["requestId"],
"trace-openai-chat-antigravity"
);
assert_eq!(payload.provider_request_body["model"], "gemini-2.5-pro");
assert_eq!(
payload.provider_request_body["userAgent"],
"antigravity/cli/1.0.16 (aidev_client; os_type=linux; arch=arm64; auth_method=consumer)"
);
assert_eq!(payload.provider_request_body["requestType"], "agent");
assert!(payload.provider_request_body.get("contents").is_none());
assert!(payload.provider_request_body["request"]
.get("contents")
.is_some());
assert!(payload.provider_request_body["request"]
.get("model")
.is_none());
}
#[test] #[test]
fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() { fn chatgpt_web_chat_image_bridge_body_uses_internal_web_shape() {
let body_json = json!({ let body_json = json!({
@@ -31,11 +31,12 @@ pub(super) fn openai_chat_upstream_is_stream_for_candidate(
crate::ai_serving::transport::kiro::is_kiro_claude_messages_transport( crate::ai_serving::transport::kiro::is_kiro_claude_messages_transport(
transport, transport,
provider_api_format, provider_api_format,
) || openai_chat_gemini_cli_client_stream_requires_upstream_streaming( ) || openai_chat_antigravity_requires_upstream_streaming(transport, provider_api_format)
transport, || openai_chat_gemini_cli_client_stream_requires_upstream_streaming(
provider_api_format, transport,
client_is_stream, provider_api_format,
); client_is_stream,
);
resolve_upstream_is_stream_for_provider( resolve_upstream_is_stream_for_provider(
transport.endpoint.config.as_ref(), transport.endpoint.config.as_ref(),
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
@@ -45,6 +46,15 @@ pub(super) fn openai_chat_upstream_is_stream_for_candidate(
) )
} }
fn openai_chat_antigravity_requires_upstream_streaming(
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> bool {
crate::ai_serving::transport::antigravity::is_antigravity_provider_transport(transport)
&& crate::ai_serving::normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
}
fn openai_chat_gemini_cli_client_stream_requires_upstream_streaming( fn openai_chat_gemini_cli_client_stream_requires_upstream_streaming(
transport: &GatewayProviderTransportSnapshot, transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str, provider_api_format: &str,
@@ -201,4 +211,24 @@ mod tests {
false, false,
)); ));
} }
#[test]
fn openai_chat_policy_resolver_preserves_antigravity_streaming_envelope() {
let antigravity = sample_transport(
"antigravity",
"gemini:generate_content",
Some(json!({"upstream_stream_policy": "force_non_stream"})),
);
assert!(openai_chat_upstream_is_stream_for_candidate(
&antigravity,
"gemini:generate_content",
false,
));
assert!(openai_chat_upstream_is_stream_for_candidate(
&antigravity,
"gemini:generate_content",
true,
));
}
} }
@@ -5,6 +5,10 @@ use aether_contracts::ResolvedTransportProfile;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tracing::debug; use tracing::debug;
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,
@@ -29,13 +33,7 @@ use crate::ai_serving::planner::standard::{
build_local_openai_responses_upstream_url, request_body_build_failure_extra_data, build_local_openai_responses_upstream_url, request_body_build_failure_extra_data,
request_conversion_failure_extra_data, request_conversion_failure_extra_data,
}; };
use crate::ai_serving::transport::antigravity::{ use crate::ai_serving::transport::antigravity::is_antigravity_provider_transport;
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
classify_local_antigravity_request_support, is_antigravity_provider_transport,
AntigravityEnvelopeRequestType, AntigravityRequestAuthUnsupportedReason,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport,
AntigravityRequestSideUnsupportedReason,
};
use crate::ai_serving::transport::auth::{ use crate::ai_serving::transport::auth::{
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth, resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
@@ -73,8 +71,6 @@ use super::support::{
}; };
use super::LocalOpenAiResponsesSpec; use super::LocalOpenAiResponsesSpec;
const ANTIGRAVITY_ENVELOPE_NAME: &str = "antigravity:v1internal";
fn is_grok_text_provider_api_format(provider_api_format: &str) -> bool { fn is_grok_text_provider_api_format(provider_api_format: &str) -> bool {
matches!( matches!(
crate::ai_serving::normalize_api_format_alias(provider_api_format).as_str(), crate::ai_serving::normalize_api_format_alias(provider_api_format).as_str(),
@@ -120,7 +116,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
let provider_api_format = eligible.provider_api_format.as_str(); let provider_api_format = eligible.provider_api_format.as_str();
let normalized_provider_api_format = let normalized_provider_api_format =
crate::ai_serving::normalize_api_format_alias(provider_api_format); crate::ai_serving::normalize_api_format_alias(provider_api_format);
let mut transport = Arc::clone(&eligible.transport); let transport = Arc::clone(&eligible.transport);
let transport_profile = crate::ai_serving::transport::resolve_transport_profile(&transport); let transport_profile = crate::ai_serving::transport::resolve_transport_profile(&transport);
let is_antigravity = is_antigravity_provider_transport(&transport); let is_antigravity = is_antigravity_provider_transport(&transport);
let is_gemini_cli = is_gemini_cli_provider_transport(&transport); let is_gemini_cli = is_gemini_cli_provider_transport(&transport);
@@ -415,82 +411,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
provider_api_format, provider_api_format,
Some(body_json), Some(body_json),
); );
let antigravity_auth = if is_antigravity { let provider_request_body = base_provider_request_body;
let mut antigravity_support = classify_local_antigravity_request_support(
&transport,
&base_provider_request_body,
AntigravityEnvelopeRequestType::Agent,
);
if matches!(
antigravity_support,
AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::MissingProjectId
)
)
) {
if let Some(hydrated) = state
.hydrate_antigravity_project_metadata_for_transport(&transport)
.await
{
transport = Arc::new(hydrated);
antigravity_support = classify_local_antigravity_request_support(
&transport,
&base_provider_request_body,
AntigravityEnvelopeRequestType::Agent,
);
}
}
match antigravity_support {
AntigravityRequestSideSupport::Supported(spec) => Some(spec.auth),
AntigravityRequestSideSupport::Unsupported(_) => {
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_unsupported",
)
.await;
return Ok(None);
}
}
} else {
None
};
let provider_request_body = if let Some(antigravity_auth) = antigravity_auth.as_ref() {
match build_antigravity_safe_v1internal_request(
antigravity_auth,
trace_id,
&mapped_model,
&base_provider_request_body,
AntigravityEnvelopeRequestType::Agent,
) {
AntigravityRequestEnvelopeSupport::Supported(envelope) => envelope,
AntigravityRequestEnvelopeSupport::Unsupported(_) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
spec_metadata.api_format,
provider_api_format,
"openai_responses_antigravity_envelope",
),
)
.await;
return Ok(None);
}
}
} else {
base_provider_request_body
};
if let Some(kiro_auth) = kiro_auth.as_ref() { if let Some(kiro_auth) = kiro_auth.as_ref() {
return build_kiro_openai_responses_payload_parts( return build_kiro_openai_responses_payload_parts(
@@ -562,6 +483,28 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
) )
.await); .await);
} }
if is_antigravity {
return Ok(build_antigravity_openai_responses_payload_parts(
state,
parts,
trace_id,
body_json,
input,
eligible,
candidate_index,
candidate_id,
spec_metadata.api_format,
&transport,
provider_api_format,
mapped_model,
auth_header,
auth_value,
provider_request_body,
upstream_is_stream,
redaction.redacted,
)
.await);
}
let Some(upstream_url) = (if is_grok && is_grok_text_provider_api_format(provider_api_format) { let Some(upstream_url) = (if is_grok && is_grok_text_provider_api_format(provider_api_format) {
Some(build_grok_upstream_url(&transport, GROK_CHAT_PATH)) Some(build_grok_upstream_url(&transport, GROK_CHAT_PATH))
@@ -598,10 +541,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
.await; .await;
return Ok(None); return Ok(None);
}; };
let extra_headers = antigravity_auth let extra_headers = BTreeMap::new();
.as_ref()
.map(build_antigravity_static_identity_headers)
.unwrap_or_default();
let resolved_headers = if is_grok && is_grok_text_provider_api_format(provider_api_format) { let resolved_headers = if is_grok && is_grok_text_provider_api_format(provider_api_format) {
let Some(headers) = build_grok_browser_headers(GrokHeaderInput { let Some(headers) = build_grok_browser_headers(GrokHeaderInput {
transport: &transport, transport: &transport,
@@ -728,13 +668,8 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
upstream_url, upstream_url,
execution_strategy, execution_strategy,
conversion_mode, conversion_mode,
is_antigravity: is_antigravity is_antigravity: false,
|| antigravity_auth.is_some() && ANTIGRAVITY_ENVELOPE_NAME == "antigravity:v1internal", envelope_name: None,
envelope_name: if is_antigravity || antigravity_auth.is_some() {
Some(ANTIGRAVITY_ENVELOPE_NAME)
} else {
None
},
upstream_is_stream, upstream_is_stream,
transport: Arc::clone(&transport), transport: Arc::clone(&transport),
transport_profile, transport_profile,
@@ -743,6 +678,160 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
})) }))
} }
#[allow(clippy::too_many_arguments)]
async fn build_antigravity_openai_responses_payload_parts(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
original_body_json: &serde_json::Value,
input: &LocalOpenAiResponsesDecisionInput,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
client_api_format: &str,
transport: &Arc<GatewayProviderTransportSnapshot>,
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<LocalOpenAiResponsesCandidatePayloadParts> {
let candidate = &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: api_format_alias_matches(provider_api_format, client_api_format),
})
.await
{
Ok(resolved) => resolved,
Err(AntigravityV1InternalRequestError::TransportUnsupported) => {
mark_skipped_local_openai_responses_candidate(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_unsupported",
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::EnvelopeUnsupported) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"provider_request_body_build_failed",
CandidateFailureDiagnostic::envelope_build_failed(
client_api_format,
provider_api_format,
"openai_responses_antigravity_envelope",
),
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::UpstreamUrlUnavailable) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"upstream_url_missing",
CandidateFailureDiagnostic::upstream_url_missing(
client_api_format,
provider_api_format,
"openai_responses_antigravity_url",
),
)
.await;
return None;
}
Err(AntigravityV1InternalRequestError::HeaderRulesApplyFailed) => {
mark_skipped_local_openai_responses_candidate_with_failure_diagnostic(
state,
input,
trace_id,
candidate,
candidate_index,
candidate_id,
"transport_header_rules_apply_failed",
CandidateFailureDiagnostic::header_rules_apply_failed(
client_api_format,
provider_api_format,
"openai_responses_antigravity_headers",
),
)
.await;
return None;
}
};
let mut provider_request_headers = resolved.headers.headers;
apply_codex_openai_responses_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(),
);
apply_local_auth_config_header_overrides(
&mut provider_request_headers,
resolved.transport.key.decrypted_auth_config.as_deref(),
);
provider_request_headers.insert("accept".to_string(), "text/event-stream".to_string());
request_identity_response_encoding_when_redacted(
&mut provider_request_headers,
request_redacted,
);
let (execution_strategy, conversion_mode) =
ai_local_execution_contract_for_formats(client_api_format, provider_api_format);
Some(LocalOpenAiResponsesCandidatePayloadParts {
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,
execution_strategy,
conversion_mode,
is_antigravity: true,
envelope_name: Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
upstream_is_stream,
transport: Arc::clone(&resolved.transport),
transport_profile: crate::ai_serving::transport::resolve_transport_profile(
&resolved.transport,
),
image_request_summary: None,
request_redacted,
})
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn build_gemini_cli_openai_responses_payload_parts( async fn build_gemini_cli_openai_responses_payload_parts(
state: &AppState, state: &AppState,
+2
View File
@@ -1,6 +1,7 @@
pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> { pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
match crate::ai_serving::normalize_api_format_alias(api_format).as_str() { match crate::ai_serving::normalize_api_format_alias(api_format).as_str() {
"gemini:generate_content" => Some("gemini:generate_content"), "gemini:generate_content" => Some("gemini:generate_content"),
"gemini:interactions" => Some("gemini:interactions"),
"gemini:embedding" => Some("gemini:embedding"), "gemini:embedding" => Some("gemini:embedding"),
"gemini:video" => Some("gemini:video"), "gemini:video" => Some("gemini:video"),
"gemini:files" => Some("gemini:files"), "gemini:files" => Some("gemini:files"),
@@ -11,6 +12,7 @@ pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
pub(crate) fn local_path(api_format: &str) -> Option<&'static str> { pub(crate) fn local_path(api_format: &str) -> Option<&'static str> {
match crate::ai_serving::normalize_api_format_alias(api_format).as_str() { match crate::ai_serving::normalize_api_format_alias(api_format).as_str() {
"gemini" | "gemini:generate_content" => Some("/v1beta/models/{model}:{action}"), "gemini" | "gemini:generate_content" => Some("/v1beta/models/{model}:{action}"),
"gemini:interactions" => Some("/v1/interactions"),
"gemini:embedding" => Some("/v1beta/models/{model}:{action}"), "gemini:embedding" => Some("/v1beta/models/{model}:{action}"),
"gemini:video" => Some("/v1beta/models/{model}:predictLongRunning"), "gemini:video" => Some("/v1beta/models/{model}:predictLongRunning"),
"gemini:files" => Some("/v1beta/files"), "gemini:files" => Some("/v1beta/files"),
@@ -17,13 +17,17 @@ const AI_POST_ROUTE_PATTERNS: &[&str] = &[
"/v1/responses/compact", "/v1/responses/compact",
"/v1/images/generations", "/v1/images/generations",
"/v1/images/edits", "/v1/images/edits",
"/v1/interactions",
"/v1beta/interactions",
"/v1internal:loadCodeAssist", "/v1internal:loadCodeAssist",
"/v1internal:fetchAvailableModels", "/v1internal:fetchAvailableModels",
"/v1internal:retrieveUserQuotaSummary",
"/v1internal:fetchUserInfo", "/v1internal:fetchUserInfo",
"/v1internal:fetchAdminControls", "/v1internal:fetchAdminControls",
"/v1internal:setUserSettings", "/v1internal:setUserSettings",
"/v1internal:listExperiments", "/v1internal:listExperiments",
"/v1internal:recordCodeAssistMetrics", "/v1internal:recordCodeAssistMetrics",
"/v1internal:writeTrajectoryAcls",
"/v1internal:streamGenerateContent", "/v1internal:streamGenerateContent",
]; ];
@@ -95,6 +99,12 @@ mod tests {
fn supports_data_api_endpoint_signatures_and_public_paths() { fn supports_data_api_endpoint_signatures_and_public_paths() {
for (api_format, family, kind, path) in [ for (api_format, family, kind, path) in [
("openai:embedding", "openai", "embedding", "/v1/embeddings"), ("openai:embedding", "openai", "embedding", "/v1/embeddings"),
(
"gemini:interactions",
"gemini",
"interactions",
"/v1/interactions",
),
( (
"gemini:embedding", "gemini:embedding",
"gemini", "gemini",
+2
View File
@@ -141,11 +141,13 @@ pub(crate) const RUST_FRONTDOOR_OWNED_ROUTE_PATTERNS: &[&str] = &[
"/v1beta/files/{path...}", "/v1beta/files/{path...}",
"/v1internal:loadCodeAssist", "/v1internal:loadCodeAssist",
"/v1internal:fetchAvailableModels", "/v1internal:fetchAvailableModels",
"/v1internal:retrieveUserQuotaSummary",
"/v1internal:fetchUserInfo", "/v1internal:fetchUserInfo",
"/v1internal:fetchAdminControls", "/v1internal:fetchAdminControls",
"/v1internal:setUserSettings", "/v1internal:setUserSettings",
"/v1internal:listExperiments", "/v1internal:listExperiments",
"/v1internal:recordCodeAssistMetrics", "/v1internal:recordCodeAssistMetrics",
"/v1internal:writeTrajectoryAcls",
"/v1internal:streamGenerateContent", "/v1internal:streamGenerateContent",
"/", "/",
"/{*path}", "/{*path}",
@@ -97,6 +97,17 @@ pub(super) fn classify_ai_public_route(
"openai:video", "openai:video",
true, true,
)) ))
} else if method == http::Method::POST
&& matches!(normalized_path, "/v1/interactions" | "/v1beta/interactions")
{
Some(classified_with_request_auth_channel(
"ai_public",
"gemini",
"interactions",
"api_key",
"gemini:interactions",
true,
))
} else if method == http::Method::POST && is_gemini_models_route(normalized_path) { } else if method == http::Method::POST && is_gemini_models_route(normalized_path) {
if normalized_path.ends_with(":predictLongRunning") { if normalized_path.ends_with(":predictLongRunning") {
Some(classified( Some(classified(
@@ -182,11 +193,13 @@ fn classify_antigravity_v1internal_route(
let (route_kind, execution_runtime_candidate) = match action { let (route_kind, execution_runtime_candidate) = match action {
"loadCodeAssist" => ("load_code_assist", false), "loadCodeAssist" => ("load_code_assist", false),
"fetchAvailableModels" => ("fetch_available_models", false), "fetchAvailableModels" => ("fetch_available_models", false),
"retrieveUserQuotaSummary" => ("retrieve_user_quota_summary", false),
"fetchUserInfo" => ("fetch_user_info", false), "fetchUserInfo" => ("fetch_user_info", false),
"fetchAdminControls" => ("fetch_admin_controls", false), "fetchAdminControls" => ("fetch_admin_controls", false),
"setUserSettings" => ("set_user_settings", false), "setUserSettings" => ("set_user_settings", false),
"listExperiments" => ("list_experiments", false), "listExperiments" => ("list_experiments", false),
"recordCodeAssistMetrics" => ("record_code_assist_metrics", false), "recordCodeAssistMetrics" => ("record_code_assist_metrics", false),
"writeTrajectoryAcls" => ("write_trajectory_acls", false),
"streamGenerateContent" => ("stream_generate_content", true), "streamGenerateContent" => ("stream_generate_content", true),
_ => return None, _ => return None,
}; };
@@ -274,6 +274,23 @@ fn classifies_gemini_batch_embed_contents_as_embedding_route() {
assert!(decision.is_execution_runtime_candidate()); assert!(decision.is_execution_runtime_candidate());
} }
#[test]
fn classifies_gemini_interactions_as_interactions_route() {
let headers = headers(&[("x-goog-api-key", "gemini-key")]);
let uri: Uri = "/v1/interactions".parse().expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_family.as_deref(), Some("gemini"));
assert_eq!(decision.route_kind.as_deref(), Some("interactions"));
assert_eq!(decision.request_auth_channel.as_deref(), Some("api_key"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("gemini:interactions")
);
assert!(decision.is_execution_runtime_candidate());
}
#[test] #[test]
fn classifies_gemini_predict_long_running_as_video_route() { fn classifies_gemini_predict_long_running_as_video_route() {
let headers = headers(&[]); let headers = headers(&[]);
@@ -302,6 +319,10 @@ fn classifies_antigravity_v1internal_control_plane_routes() {
for (path, route_kind) in [ for (path, route_kind) in [
("/v1internal:loadCodeAssist", "load_code_assist"), ("/v1internal:loadCodeAssist", "load_code_assist"),
("/v1internal:fetchAvailableModels", "fetch_available_models"), ("/v1internal:fetchAvailableModels", "fetch_available_models"),
(
"/v1internal:retrieveUserQuotaSummary",
"retrieve_user_quota_summary",
),
("/v1internal:fetchUserInfo", "fetch_user_info"), ("/v1internal:fetchUserInfo", "fetch_user_info"),
("/v1internal:fetchAdminControls", "fetch_admin_controls"), ("/v1internal:fetchAdminControls", "fetch_admin_controls"),
("/v1internal:setUserSettings", "set_user_settings"), ("/v1internal:setUserSettings", "set_user_settings"),
@@ -310,6 +331,7 @@ fn classifies_antigravity_v1internal_control_plane_routes() {
"/v1internal:recordCodeAssistMetrics", "/v1internal:recordCodeAssistMetrics",
"record_code_assist_metrics", "record_code_assist_metrics",
), ),
("/v1internal:writeTrajectoryAcls", "write_trajectory_acls"),
] { ] {
let uri: Uri = path.parse().expect("uri should parse"); let uri: Uri = path.parse().expect("uri should parse");
let decision = classify_control_route(&http::Method::POST, &uri, &headers) let decision = classify_control_route(&http::Method::POST, &uri, &headers)
@@ -179,6 +179,7 @@ pub(crate) fn resolve_core_sync_error_finalize_report_kind(
"openai_responses_compact_sync" => "openai_responses_compact_sync_finalize", "openai_responses_compact_sync" => "openai_responses_compact_sync_finalize",
"claude_chat_sync" => "claude_chat_sync_finalize", "claude_chat_sync" => "claude_chat_sync_finalize",
"gemini_chat_sync" => "gemini_chat_sync_finalize", "gemini_chat_sync" => "gemini_chat_sync_finalize",
"gemini_interactions_sync" => "gemini_interactions_sync_finalize",
"claude_cli_sync" => "claude_cli_sync_finalize", "claude_cli_sync" => "claude_cli_sync_finalize",
"gemini_cli_sync" => "gemini_cli_sync_finalize", "gemini_cli_sync" => "gemini_cli_sync_finalize",
_ => return None, _ => return None,
@@ -324,6 +325,7 @@ pub(crate) fn resolve_core_stream_error_finalize_report_kind(
"openai_chat_stream" => "openai_chat_sync_finalize", "openai_chat_stream" => "openai_chat_sync_finalize",
"claude_chat_stream" => "claude_chat_sync_finalize", "claude_chat_stream" => "claude_chat_sync_finalize",
"gemini_chat_stream" => "gemini_chat_sync_finalize", "gemini_chat_stream" => "gemini_chat_sync_finalize",
"gemini_interactions_stream" => "gemini_interactions_sync_finalize",
"openai_responses_stream" => "openai_responses_sync_finalize", "openai_responses_stream" => "openai_responses_sync_finalize",
"openai_responses_compact_stream" => "openai_responses_compact_sync_finalize", "openai_responses_compact_stream" => "openai_responses_compact_sync_finalize",
"claude_cli_stream" => "claude_cli_sync_finalize", "claude_cli_stream" => "claude_cli_sync_finalize",
@@ -340,6 +342,7 @@ pub(crate) fn resolve_core_stream_direct_finalize_report_kind(plan_kind: &str) -
"openai_image_stream" => "openai_image_sync_finalize", "openai_image_stream" => "openai_image_sync_finalize",
"claude_chat_stream" => "claude_chat_sync_finalize", "claude_chat_stream" => "claude_chat_sync_finalize",
"gemini_chat_stream" => "gemini_chat_sync_finalize", "gemini_chat_stream" => "gemini_chat_sync_finalize",
"gemini_interactions_stream" => "gemini_interactions_sync_finalize",
"openai_responses_stream" => "openai_responses_sync_finalize", "openai_responses_stream" => "openai_responses_sync_finalize",
"openai_responses_compact_stream" => "openai_responses_compact_sync_finalize", "openai_responses_compact_stream" => "openai_responses_compact_sync_finalize",
"claude_cli_stream" => "claude_cli_sync_finalize", "claude_cli_stream" => "claude_cli_sync_finalize",
@@ -2637,7 +2637,20 @@ async fn execute_execution_runtime_sync_impl(
candidate_id, candidate_id,
)?; )?;
if !usage_payload.report_kind.trim().is_empty() { if !usage_payload.report_kind.trim().is_empty() {
spawn_sync_report(state.clone(), usage_payload); if status_code >= 400 {
let report_kind = usage_payload.report_kind.clone();
if let Err(err) = submit_sync_report(state, usage_payload).await {
warn!(
event_name = "local_sync_error_report_submit_failed",
log_type = "ops",
trace_id = %trace_id,
report_kind = %report_kind,
"gateway failed to submit local sync error report before returning response: {err:?}"
);
}
} else {
spawn_sync_report(state.clone(), usage_payload);
}
} }
Ok(Some(response)) Ok(Some(response))
@@ -104,6 +104,10 @@ fn resolve_core_error_background_report_kind_maps_all_core_finalize_kinds() {
("openai_chat_sync_finalize", Some("openai_chat_sync_error")), ("openai_chat_sync_finalize", Some("openai_chat_sync_error")),
("claude_chat_sync_finalize", Some("claude_chat_sync_error")), ("claude_chat_sync_finalize", Some("claude_chat_sync_error")),
("gemini_chat_sync_finalize", Some("gemini_chat_sync_error")), ("gemini_chat_sync_finalize", Some("gemini_chat_sync_error")),
(
"gemini_interactions_sync_finalize",
Some("gemini_interactions_sync_error"),
),
( (
"openai_responses_sync_finalize", "openai_responses_sync_finalize",
Some("openai_responses_sync_error"), Some("openai_responses_sync_error"),
@@ -151,6 +155,10 @@ fn resolve_core_success_background_report_kind_maps_all_core_finalize_kinds() {
"gemini_chat_sync_finalize", "gemini_chat_sync_finalize",
Some("gemini_chat_sync_success"), Some("gemini_chat_sync_success"),
), ),
(
"gemini_interactions_sync_finalize",
Some("gemini_interactions_sync_success"),
),
( (
"openai_responses_sync_finalize", "openai_responses_sync_finalize",
Some("openai_responses_sync_success"), Some("openai_responses_sync_success"),
@@ -36,8 +36,8 @@ use crate::provider_key_auth::{
use crate::provider_transport::antigravity::{ use crate::provider_transport::antigravity::{
build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers, build_antigravity_safe_v1internal_request, build_antigravity_static_identity_headers,
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType, classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestEnvelopeSupport, AntigravityRequestSideSupport, AntigravityRequestAuthUnsupportedReason, AntigravityRequestEnvelopeSupport,
AntigravityRequestSideUnsupportedReason, AntigravityRequestSideSupport, AntigravityRequestSideUnsupportedReason,
}; };
use crate::provider_transport::kiro::{ use crate::provider_transport::kiro::{
build_kiro_generate_assistant_response_url, build_kiro_provider_headers, build_kiro_generate_assistant_response_url, build_kiro_provider_headers,
@@ -2061,7 +2061,7 @@ async fn provider_query_execute_openai_image_test_candidate(
trace_id: &str, trace_id: &str,
requested_model: &str, requested_model: &str,
) -> Result<ProviderQueryExecutionOutcome, GatewayError> { ) -> Result<ProviderQueryExecutionOutcome, GatewayError> {
let Some(transport) = state let Some(mut transport) = state
.read_provider_transport_snapshot(&provider.id, &candidate.endpoint.id, &candidate.key.id) .read_provider_transport_snapshot(&provider.id, &candidate.endpoint.id, &candidate.key.id)
.await? .await?
else { else {
@@ -2429,7 +2429,7 @@ async fn provider_query_execute_antigravity_test_candidate(
trace_id: &str, trace_id: &str,
requested_model: &str, requested_model: &str,
) -> Result<ProviderQueryExecutionOutcome, GatewayError> { ) -> Result<ProviderQueryExecutionOutcome, GatewayError> {
let Some(transport) = state let Some(mut transport) = state
.read_provider_transport_snapshot(&provider.id, &candidate.endpoint.id, &candidate.key.id) .read_provider_transport_snapshot(&provider.id, &candidate.endpoint.id, &candidate.key.id)
.await? .await?
else { else {
@@ -2463,11 +2463,34 @@ async fn provider_query_execute_antigravity_test_candidate(
)); ));
}; };
let antigravity_spec = match classify_local_antigravity_request_support( let mut antigravity_support = classify_local_antigravity_request_support(
&transport, &transport,
&base_provider_request_body, &base_provider_request_body,
AntigravityEnvelopeRequestType::EndpointTest, AntigravityEnvelopeRequestType::EndpointTest,
);
if matches!(
antigravity_support,
AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::MissingProjectId
)
)
) { ) {
if let Some(hydrated) = state
.app()
.hydrate_antigravity_project_metadata_for_transport(&transport)
.await
{
transport = hydrated;
antigravity_support = classify_local_antigravity_request_support(
&transport,
&base_provider_request_body,
AntigravityEnvelopeRequestType::EndpointTest,
);
}
}
let antigravity_spec = match antigravity_support {
AntigravityRequestSideSupport::Supported(spec) => spec, AntigravityRequestSideSupport::Supported(spec) => spec,
AntigravityRequestSideSupport::Unsupported(reason) => { AntigravityRequestSideSupport::Unsupported(reason) => {
let reason = provider_query_antigravity_unsupported_reason(reason); let reason = provider_query_antigravity_unsupported_reason(reason);
@@ -2,7 +2,8 @@ use super::DEFAULT_PROVIDER_QUERY_TEST_MESSAGE;
use crate::handlers::admin::request::{AdminAppState, AdminGatewayProviderTransportSnapshot}; use crate::handlers::admin::request::{AdminAppState, AdminGatewayProviderTransportSnapshot};
use crate::provider_transport::antigravity::{ use crate::provider_transport::antigravity::{
classify_local_antigravity_request_support, AntigravityEnvelopeRequestType, classify_local_antigravity_request_support, AntigravityEnvelopeRequestType,
AntigravityRequestSideSupport, AntigravityRequestSideUnsupportedReason, AntigravityRequestAuthUnsupportedReason, AntigravityRequestSideSupport,
AntigravityRequestSideUnsupportedReason,
}; };
use crate::provider_transport::kiro::supports_local_kiro_request_transport_with_network; use crate::provider_transport::kiro::supports_local_kiro_request_transport_with_network;
use serde_json::{json, Value}; use serde_json::{json, Value};
@@ -82,14 +83,14 @@ pub(super) fn provider_query_standard_test_unsupported_reason(
api_format, api_format,
) )
} }
"gemini:generate_content" | "gemini:embedding" "gemini:generate_content" | "gemini:embedding" | "gemini:interactions"
if crate::provider_transport::is_vertex_transport_context(transport) => if crate::provider_transport::is_vertex_transport_context(transport) =>
{ {
aether_provider_transport::vertex::local_vertex_gemini_transport_unsupported_reason_with_network( aether_provider_transport::vertex::local_vertex_gemini_transport_unsupported_reason_with_network(
transport, transport,
) )
} }
"gemini:generate_content" | "gemini:embedding" => { "gemini:generate_content" | "gemini:embedding" | "gemini:interactions" => {
crate::provider_transport::policy::local_gemini_transport_unsupported_reason_with_network( crate::provider_transport::policy::local_gemini_transport_unsupported_reason_with_network(
transport, transport,
api_format, api_format,
@@ -130,9 +131,21 @@ pub(super) fn provider_query_antigravity_unsupported_reason(
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig => { AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig => {
"transport_network_config_unsupported" "transport_network_config_unsupported"
} }
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(_) => { AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
"transport_antigravity_auth_unsupported" AntigravityRequestAuthUnsupportedReason::WrongProviderType,
} ) => "transport_provider_type_unsupported",
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::MissingAuthConfig,
) => "transport_antigravity_auth_config_missing",
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::InvalidAuthConfigJson,
) => "transport_antigravity_auth_config_invalid",
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::ComplexDynamicAuthConfig,
) => "transport_antigravity_auth_config_unsupported",
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(
AntigravityRequestAuthUnsupportedReason::MissingProjectId,
) => "transport_antigravity_project_id_missing",
AntigravityRequestSideUnsupportedReason::UnsupportedEnvelope(_) => { AntigravityRequestSideUnsupportedReason::UnsupportedEnvelope(_) => {
"transport_antigravity_envelope_unsupported" "transport_antigravity_envelope_unsupported"
} }
@@ -250,6 +263,7 @@ pub(super) fn provider_query_test_adapter_for_provider_api_format(
| "openai:responses:compact" | "openai:responses:compact"
| "claude:messages" | "claude:messages"
| "gemini:generate_content" | "gemini:generate_content"
| "gemini:interactions"
| "openai:embedding" | "openai:embedding"
| "gemini:embedding" | "gemini:embedding"
| "jina:embedding" | "jina:embedding"
@@ -366,7 +380,7 @@ pub(super) fn provider_query_transport_supports_model_test_execution(
transport, api_format, transport, api_format,
) )
} }
"gemini:generate_content" | "gemini:embedding" => { "gemini:generate_content" | "gemini:embedding" | "gemini:interactions" => {
if crate::provider_transport::is_vertex_transport_context(transport) { if crate::provider_transport::is_vertex_transport_context(transport) {
aether_provider_transport::vertex::supports_local_vertex_gemini_transport_with_network(transport) aether_provider_transport::vertex::supports_local_vertex_gemini_transport_with_network(transport)
} else { } else {
@@ -120,6 +120,10 @@ fn provider_query_endpoint_route_payload(
("Gemini API", "gemini_native", "generateContent", "") ("Gemini API", "gemini_native", "generateContent", "")
} }
"gemini:generate_content" => ("Gemini native", "gemini_native", "generateContent", ""), "gemini:generate_content" => ("Gemini native", "gemini_native", "generateContent", ""),
"gemini:interactions" if is_gemini_api => {
("Gemini API", "gemini_native", "interactions", "")
}
"gemini:interactions" => ("Gemini native", "gemini_native", "interactions", ""),
"openai:embedding" if is_vertex && is_openai_compat => ( "openai:embedding" if is_vertex && is_openai_compat => (
"Vertex AI OpenAI-compatible", "Vertex AI OpenAI-compatible",
"openai_compatible", "openai_compatible",
@@ -678,6 +678,10 @@ fn provider_query_test_adapter_routes_fixed_provider_endpoint_types() {
provider_query_test_adapter_for_provider_api_format("custom", "gemini:embedding"), provider_query_test_adapter_for_provider_api_format("custom", "gemini:embedding"),
Some(ProviderQueryTestAdapter::Standard) Some(ProviderQueryTestAdapter::Standard)
); );
assert_eq!(
provider_query_test_adapter_for_provider_api_format("custom", "gemini:interactions"),
Some(ProviderQueryTestAdapter::Standard)
);
assert_eq!( assert_eq!(
provider_query_test_adapter_for_provider_api_format( provider_query_test_adapter_for_provider_api_format(
"aliyun", "aliyun",
@@ -926,6 +926,9 @@ fn maybe_build_local_antigravity_v1internal_response(
"fetch_available_models" => { "fetch_available_models" => {
Some(Json(build_antigravity_fetch_available_models_payload()).into_response()) Some(Json(build_antigravity_fetch_available_models_payload()).into_response())
} }
"retrieve_user_quota_summary" => {
Some(Json(build_antigravity_retrieve_user_quota_summary_payload()).into_response())
}
"fetch_user_info" => { "fetch_user_info" => {
Some(Json(build_antigravity_fetch_user_info_payload()).into_response()) Some(Json(build_antigravity_fetch_user_info_payload()).into_response())
} }
@@ -938,6 +941,7 @@ fn maybe_build_local_antigravity_v1internal_response(
.into_response(), .into_response(),
), ),
"record_code_assist_metrics" => Some(Json(json!({})).into_response()), "record_code_assist_metrics" => Some(Json(json!({})).into_response()),
"write_trajectory_acls" => Some(Json(json!({})).into_response()),
"set_user_settings" => Some(build_antigravity_set_user_settings_response(request_body)), "set_user_settings" => Some(build_antigravity_set_user_settings_response(request_body)),
"stream_generate_content" => None, "stream_generate_content" => None,
_ => None, _ => None,
@@ -1042,27 +1046,42 @@ fn build_antigravity_fetch_user_info_payload() -> Value {
}) })
} }
fn build_antigravity_retrieve_user_quota_summary_payload() -> Value {
json!({
"description": "",
"groups": []
})
}
fn build_antigravity_default_user_settings_payload() -> Value { fn build_antigravity_default_user_settings_payload() -> Value {
json!({ json!({
"preferredModelId": "gemini-3.1-flash-lite" "preferredModelId": "gemini-3.5-flash-low"
}) })
} }
fn build_antigravity_fetch_available_models_payload() -> Value { fn build_antigravity_fetch_available_models_payload() -> Value {
json!({ json!({
"models": { "models": {
"gemini-3.5-flash-low": antigravity_model_payload("gemini-3.5-flash-low", "Gemini 3.5 Flash Low"), "gemini-pro-agent": antigravity_model_payload("gemini-pro-agent", "Gemini 3.1 Pro (High)"),
"gemini-3-flash-agent": antigravity_model_payload("gemini-3-flash-agent", "Gemini 3 Flash Agent"), "gemini-3.1-pro-low": antigravity_model_payload("gemini-3.1-pro-low", "Gemini 3.1 Pro (Low)"),
"gemini-3-flash-agent": antigravity_model_payload("gemini-3-flash-agent", "Gemini 3.5 Flash (High)"),
"gemini-3.5-flash-low": antigravity_model_payload("gemini-3.5-flash-low", "Gemini 3.5 Flash (Medium)"),
"gemini-3.5-flash-extra-low": antigravity_model_payload("gemini-3.5-flash-extra-low", "Gemini 3.5 Flash (Low)"),
"claude-opus-4-6-thinking": antigravity_model_payload("claude-opus-4-6-thinking", "Claude Opus 4.6 (Thinking)"),
"claude-sonnet-4-6": antigravity_model_payload("claude-sonnet-4-6", "Claude Sonnet 4.6 (Thinking)"),
"gpt-oss-120b-medium": antigravity_model_payload("gpt-oss-120b-medium", "GPT-OSS 120B (Medium)"),
"gemini-3.1-flash-lite": antigravity_model_payload("gemini-3.1-flash-lite", "Gemini 3.1 Flash Lite"), "gemini-3.1-flash-lite": antigravity_model_payload("gemini-3.1-flash-lite", "Gemini 3.1 Flash Lite"),
"gemini-3.1-pro-low": antigravity_model_payload("gemini-3.1-pro-low", "Gemini 3.1 Pro Low"),
"gemini-3-flash": antigravity_model_payload("gemini-3-flash", "Gemini 3 Flash"), "gemini-3-flash": antigravity_model_payload("gemini-3-flash", "Gemini 3 Flash"),
"gemini-2.5-flash": antigravity_model_payload("gemini-2.5-flash", "Gemini 2.5 Flash"), "gemini-2.5-flash": antigravity_model_payload("gemini-2.5-flash", "Gemini 3.1 Flash Lite"),
"gemini-2.5-flash-lite": antigravity_model_payload("gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite"), "gemini-2.5-flash-lite": antigravity_model_payload("gemini-2.5-flash-lite", "Gemini 3.1 Flash Lite"),
"gemini-2.5-flash-thinking": antigravity_model_payload("gemini-2.5-flash-thinking", "Gemini 2.5 Flash Thinking"), "gemini-2.5-flash-thinking": antigravity_model_payload("gemini-2.5-flash-thinking", "Gemini 3.1 Flash Lite"),
"gemini-2.5-pro": antigravity_model_payload("gemini-2.5-pro", "Gemini 2.5 Pro"), "gemini-2.5-pro": antigravity_model_payload("gemini-2.5-pro", "Gemini 2.5 Pro"),
"gemini-3.1-flash-image": antigravity_model_payload("gemini-3.1-flash-image", "Gemini 3.1 Flash Image"), "gemini-3.1-flash-image": antigravity_model_payload("gemini-3.1-flash-image", "Gemini 3.1 Flash Image"),
"tab_flash_lite_preview": antigravity_model_payload("tab_flash_lite_preview", "Tab Flash Lite Preview"), "gemini-3.1-pro-high": antigravity_model_payload("gemini-3.1-pro-high", "Gemini 3.1 Pro (High)"),
"tab_jump_flash_lite_preview": antigravity_model_payload("tab_jump_flash_lite_preview", "Tab Jump Flash Lite Preview"), "chat_20706": antigravity_model_payload("chat_20706", ""),
"chat_23310": antigravity_model_payload("chat_23310", ""),
"tab_flash_lite_preview": antigravity_model_payload("tab_flash_lite_preview", ""),
"tab_jump_flash_lite_preview": antigravity_model_payload("tab_jump_flash_lite_preview", ""),
"models/proactive-observer": antigravity_model_payload("models/proactive-observer", "Proactive Observer") "models/proactive-observer": antigravity_model_payload("models/proactive-observer", "Proactive Observer")
}, },
"agentModelSorts": [ "agentModelSorts": [
@@ -1071,10 +1090,14 @@ fn build_antigravity_fetch_available_models_payload() -> Value {
"groups": [ "groups": [
{ {
"modelIds": [ "modelIds": [
"gemini-3.1-flash-lite", "gemini-3.5-flash-low",
"gemini-3-flash-agent", "gemini-3-flash-agent",
"gemini-3.5-flash-extra-low",
"gemini-3.1-pro-low", "gemini-3.1-pro-low",
"gemini-3.5-flash-low" "gemini-pro-agent",
"claude-sonnet-4-6",
"claude-opus-4-6-thinking",
"gpt-oss-120b-medium"
] ]
} }
] ]
@@ -1083,12 +1106,18 @@ fn build_antigravity_fetch_available_models_payload() -> Value {
"audioTranscriptionModelIds": ["models/proactive-observer"], "audioTranscriptionModelIds": ["models/proactive-observer"],
"commandModelIds": ["gemini-3-flash"], "commandModelIds": ["gemini-3-flash"],
"commitMessageModelIds": ["gemini-3.1-flash-lite"], "commitMessageModelIds": ["gemini-3.1-flash-lite"],
"defaultAgentModelId": "gemini-3.1-flash-lite", "defaultAgentModelId": "gemini-3.5-flash-low",
"deprecatedModelIds": {}, "deprecatedModelIds": {
"gemini-3.1-pro-high": {
"newModelEnum": "MODEL_PLACEHOLDER_M16",
"newModelId": "gemini-pro-agent",
"oldModelEnum": "MODEL_PLACEHOLDER_M37"
}
},
"experimentIds": [], "experimentIds": [],
"imageGenerationModelIds": ["gemini-3.1-flash-image"], "imageGenerationModelIds": ["gemini-3.1-flash-image"],
"mqueryModelIds": ["gemini-3.1-flash-lite"], "mqueryModelIds": ["gemini-3.1-flash-lite"],
"tabModelIds": ["tab_flash_lite_preview", "tab_jump_flash_lite_preview"], "tabModelIds": ["chat_20706", "chat_23310"],
"tieredModelIds": { "tieredModelIds": {
"flash": ["gemini-3-flash-agent"], "flash": ["gemini-3-flash-agent"],
"flashLite": ["gemini-3.1-flash-lite"], "flashLite": ["gemini-3.1-flash-lite"],
@@ -1100,6 +1129,11 @@ fn build_antigravity_fetch_available_models_payload() -> Value {
fn antigravity_model_payload(id: &str, display_name: &str) -> Value { fn antigravity_model_payload(id: &str, display_name: &str) -> Value {
let model = match id { let model = match id {
"chat_20706" => "MODEL_CHAT_20706",
"chat_23310" => "MODEL_CHAT_23310",
"claude-opus-4-6-thinking" => "MODEL_PLACEHOLDER_M26",
"claude-sonnet-4-6" => "MODEL_PLACEHOLDER_M35",
"gpt-oss-120b-medium" => "MODEL_OPENAI_GPT_OSS_120B_MEDIUM",
"gemini-2.5-flash" => "MODEL_GOOGLE_GEMINI_2_5_FLASH", "gemini-2.5-flash" => "MODEL_GOOGLE_GEMINI_2_5_FLASH",
"gemini-2.5-flash-lite" => "MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE", "gemini-2.5-flash-lite" => "MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE",
"gemini-2.5-flash-thinking" => "MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING", "gemini-2.5-flash-thinking" => "MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING",
@@ -1108,22 +1142,43 @@ fn antigravity_model_payload(id: &str, display_name: &str) -> Value {
"gemini-3-flash-agent" => "MODEL_PLACEHOLDER_M132", "gemini-3-flash-agent" => "MODEL_PLACEHOLDER_M132",
"gemini-3.1-flash-image" => "MODEL_PLACEHOLDER_M21", "gemini-3.1-flash-image" => "MODEL_PLACEHOLDER_M21",
"gemini-3.1-flash-lite" => "MODEL_PLACEHOLDER_M50", "gemini-3.1-flash-lite" => "MODEL_PLACEHOLDER_M50",
"gemini-pro-agent" => "MODEL_PLACEHOLDER_M16",
"gemini-3.1-pro-high" => "MODEL_PLACEHOLDER_M37",
"gemini-3.1-pro-low" => "MODEL_PLACEHOLDER_M36", "gemini-3.1-pro-low" => "MODEL_PLACEHOLDER_M36",
"gemini-3.5-flash-low" => "MODEL_PLACEHOLDER_M20", "gemini-3.5-flash-low" => "MODEL_PLACEHOLDER_M20",
"gemini-3.5-flash-extra-low" => "MODEL_PLACEHOLDER_M187",
"models/proactive-observer" => "MODEL_PLACEHOLDER_M70", "models/proactive-observer" => "MODEL_PLACEHOLDER_M70",
"tab_flash_lite_preview" => "MODEL_PLACEHOLDER_M19", "tab_flash_lite_preview" => "MODEL_PLACEHOLDER_M19",
"tab_jump_flash_lite_preview" => "MODEL_PLACEHOLDER_M28", "tab_jump_flash_lite_preview" => "MODEL_PLACEHOLDER_M28",
_ => "MODEL_PLACEHOLDER_M20", _ => "MODEL_PLACEHOLDER_M20",
}; };
json!({ let (api_provider, model_provider) = match id {
"apiProvider": "API_PROVIDER_GOOGLE_GEMINI", "claude-opus-4-6-thinking" | "claude-sonnet-4-6" => {
("API_PROVIDER_ANTHROPIC_VERTEX", "MODEL_PROVIDER_ANTHROPIC")
}
"gpt-oss-120b-medium" => ("API_PROVIDER_OPENAI_VERTEX", "MODEL_PROVIDER_OPENAI"),
"chat_20706" | "chat_23310" => ("API_PROVIDER_INTERNAL", "MODEL_PROVIDER_GOOGLE"),
_ => ("API_PROVIDER_GOOGLE_GEMINI", "MODEL_PROVIDER_GOOGLE"),
};
let mut payload = json!({
"apiProvider": api_provider,
"displayName": display_name, "displayName": display_name,
"maxOutputTokens": 65536, "maxOutputTokens": 65536,
"maxTokens": 1048576, "maxTokens": 1048576,
"minThinkingBudget": 32, "minThinkingBudget": 32,
"model": model, "model": model,
"modelProvider": "MODEL_PROVIDER_GOOGLE", "modelProvider": model_provider,
"recommended": id == "gemini-3.1-flash-lite", "recommended": matches!(
id,
"gemini-3.5-flash-low"
| "gemini-3-flash-agent"
| "gemini-3.5-flash-extra-low"
| "gemini-3.1-pro-low"
| "gemini-pro-agent"
| "claude-sonnet-4-6"
| "claude-opus-4-6-thinking"
| "gpt-oss-120b-medium"
),
"supportedMimeTypes": { "supportedMimeTypes": {
"application/json": true, "application/json": true,
"application/pdf": true, "application/pdf": true,
@@ -1137,7 +1192,88 @@ fn antigravity_model_payload(id: &str, display_name: &str) -> Value {
"supportsVideo": true, "supportsVideo": true,
"thinkingBudget": 4000, "thinkingBudget": 4000,
"tokenizerType": "LLAMA_WITH_SPECIAL" "tokenizerType": "LLAMA_WITH_SPECIAL"
}) });
if let Some(object) = payload.as_object_mut() {
match id {
"gemini-3-flash-agent" => {
object.insert("thinkingBudget".to_string(), json!(10000));
}
"gemini-pro-agent" => {
object.insert("maxOutputTokens".to_string(), json!(65535));
object.insert("thinkingBudget".to_string(), json!(10001));
}
"gemini-3.1-pro-high" => {
object.insert("maxOutputTokens".to_string(), json!(65535));
object.insert("minThinkingBudget".to_string(), json!(128));
object.insert("thinkingBudget".to_string(), json!(10001));
}
"gemini-3.5-flash-extra-low" => {
object.insert("thinkingBudget".to_string(), json!(1000));
object.insert("maxOutputTokens".to_string(), json!(65536));
}
"gemini-3.1-pro-low" => {
object.insert("maxOutputTokens".to_string(), json!(65535));
object.insert("minThinkingBudget".to_string(), json!(128));
object.insert("thinkingBudget".to_string(), json!(1001));
}
"claude-sonnet-4-6" | "claude-opus-4-6-thinking" => {
object.insert("maxTokens".to_string(), json!(250000));
object.insert("maxOutputTokens".to_string(), json!(64000));
object.insert("thinkingBudget".to_string(), json!(1024));
object.remove("minThinkingBudget");
object.remove("supportsVideo");
}
"gpt-oss-120b-medium" => {
object.insert("maxTokens".to_string(), json!(131072));
object.insert("maxOutputTokens".to_string(), json!(32768));
object.insert("thinkingBudget".to_string(), json!(8192));
object.remove("minThinkingBudget");
object.remove("supportsImages");
object.remove("supportsVideo");
}
"chat_20706" => {
object.insert("maxTokens".to_string(), json!(16384));
object.remove("displayName");
object.remove("maxOutputTokens");
object.remove("minThinkingBudget");
object.remove("recommended");
object.remove("supportsImages");
object.remove("supportsThinking");
object.remove("supportsVideo");
object.remove("thinkingBudget");
object.remove("tokenizerType");
object.remove("supportedMimeTypes");
}
"chat_23310" => {
object.insert("maxTokens".to_string(), json!(32768));
object.remove("displayName");
object.remove("maxOutputTokens");
object.remove("minThinkingBudget");
object.remove("recommended");
object.remove("supportsImages");
object.remove("supportsThinking");
object.remove("supportsVideo");
object.remove("thinkingBudget");
object.remove("tokenizerType");
object.remove("supportedMimeTypes");
}
"tab_flash_lite_preview" | "tab_jump_flash_lite_preview" => {
object.insert("maxTokens".to_string(), json!(16384));
object.insert("maxOutputTokens".to_string(), json!(4096));
object.remove("displayName");
object.remove("minThinkingBudget");
object.remove("recommended");
object.remove("supportsImages");
object.remove("supportsThinking");
object.remove("supportsVideo");
object.remove("thinkingBudget");
object.remove("tokenizerType");
object.remove("supportedMimeTypes");
}
_ => {}
}
}
payload
} }
async fn maybe_build_local_gemini_video_operations_response( async fn maybe_build_local_gemini_video_operations_response(
@@ -102,7 +102,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
let format_value = crate::ai_serving::normalize_api_format_alias(&format_value); let format_value = crate::ai_serving::normalize_api_format_alias(&format_value);
if !matches!( if !matches!(
format_value.as_str(), format_value.as_str(),
"openai:chat" | "claude:messages" | "gemini:generate_content" "openai:chat" | "claude:messages" | "gemini:generate_content" | "gemini:interactions"
) { ) {
return None; return None;
} }
@@ -186,6 +186,10 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
"parts": [{"text": "Health check"}], "parts": [{"text": "Health check"}],
}], }],
}), }),
"gemini:interactions" => json!({
"model": model,
"input": "Health check",
}),
_ => return None, _ => return None,
}; };
if !crate::provider_transport::apply_local_body_rules( if !crate::provider_transport::apply_local_body_rules(
@@ -197,7 +201,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
} }
let oauth_auth = match format_value.as_str() { let oauth_auth = match format_value.as_str() {
"openai:chat" | "claude:messages" | "gemini:generate_content" => { "openai:chat" | "claude:messages" | "gemini:generate_content" | "gemini:interactions" => {
match state.resolve_local_oauth_request_auth(&transport).await { match state.resolve_local_oauth_request_auth(&transport).await {
Ok(Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Header { Ok(Some(crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
name, name,
@@ -218,7 +222,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
crate::provider_transport::auth::resolve_local_standard_auth(&transport) crate::provider_transport::auth::resolve_local_standard_auth(&transport)
.or(oauth_auth.clone()) .or(oauth_auth.clone())
} }
"gemini:generate_content" => { "gemini:generate_content" | "gemini:interactions" => {
crate::provider_transport::auth::resolve_local_gemini_auth(&transport) crate::provider_transport::auth::resolve_local_gemini_auth(&transport)
.or(oauth_auth.clone()) .or(oauth_auth.clone())
} }
@@ -577,6 +577,44 @@ fn model_quota_window_snapshot(
Some(Value::Object(window)) Some(Value::Object(window))
} }
fn canonical_antigravity_model_label(model_name: &str) -> Option<&'static str> {
match model_name.trim() {
"claude-opus-4-6-thinking" => Some("Claude Opus 4.6 (Thinking)"),
"claude-sonnet-4-6" | "claude-sonnet-4-6-thinking" => Some("Claude Sonnet 4.6 (Thinking)"),
"gemini-3-flash-agent" => Some("Gemini 3.5 Flash (High)"),
"gemini-3.5-flash-low" => Some("Gemini 3.5 Flash (Medium)"),
"gemini-3.5-flash-extra-low" => Some("Gemini 3.5 Flash (Low)"),
"gemini-3.1-pro-high" | "gemini-pro-agent" => Some("Gemini 3.1 Pro (High)"),
"gemini-3.1-pro-low" => Some("Gemini 3.1 Pro (Low)"),
"gemini-3.1-flash-image" => Some("Gemini 3.1 Flash Image"),
"gemini-3.1-flash-lite" => Some("Gemini 3.1 Flash Lite"),
"gemini-3-flash" => Some("Gemini 3 Flash"),
"gemini-2.5-pro" => Some("Gemini 2.5 Pro"),
"gemini-2.5-flash-thinking" | "gemini-2.5-flash" | "gemini-2.5-flash-lite" => {
Some("Gemini 3.1 Flash Lite")
}
"gpt-oss-120b-medium" => Some("GPT-OSS 120B (Medium)"),
"tab_flash_lite_preview" => Some("Tab Flash Lite Preview"),
"tab_jump_flash_lite_preview" => Some("Tab Jump Flash Lite Preview"),
"models/proactive-observer" => Some("Proactive Observer"),
_ => None,
}
}
fn antigravity_model_quota_window_snapshot(
model_name: &str,
item: &Map<String, Value>,
observed_at_unix_secs: Option<u64>,
) -> Option<Value> {
let mut window = model_quota_window_snapshot(model_name, item, observed_at_unix_secs)?;
if let Some(label) = canonical_antigravity_model_label(model_name) {
if let Some(window) = window.as_object_mut() {
window.insert("label".to_string(), json!(label));
}
}
Some(window)
}
fn provider_quota_metadata_string( fn provider_quota_metadata_string(
metadata: &Map<String, Value>, metadata: &Map<String, Value>,
fields: &[&str], fields: &[&str],
@@ -1466,7 +1504,7 @@ fn build_antigravity_quota_status_snapshot(
models models
.iter() .iter()
.filter_map(|(model_name, item)| { .filter_map(|(model_name, item)| {
model_quota_window_snapshot( antigravity_model_quota_window_snapshot(
model_name, model_name,
item.as_object()?, item.as_object()?,
observed_at_unix_secs, observed_at_unix_secs,
@@ -3715,6 +3753,77 @@ mod tests {
); );
} }
#[test]
fn sync_provider_key_quota_status_snapshot_labels_antigravity_models_by_model_id() {
let upstream_metadata = json!({
"antigravity": {
"updated_at": 1_775_553_285u64,
"quota_by_model": {
"gemini-3.5-flash-extra-low": {
"display_name": "Gemini 3.5 Flash (Low)",
"remaining_fraction": 1.0
},
"gemini-3.5-flash-low": {
"display_name": "Gemini 3.5 Flash (Medium)",
"remaining_fraction": 0.75
},
"gemini-3-flash-agent": {
"display_name": "Gemini 3.5 Flash (High)",
"remaining_fraction": 0.5
},
"gemini-2.5-flash": {
"display_name": "Gemini 3.1 Flash Lite",
"remaining_fraction": 0.4
},
"claude-sonnet-4-6": {
"display_name": "Claude Sonnet 4.6 (Thinking)",
"remaining_fraction": 0.3
}
}
}
});
let payload = sync_provider_key_quota_status_snapshot(
None,
"antigravity",
Some(&upstream_metadata),
"refresh_api",
)
.expect("quota snapshot should sync");
let windows = payload["quota"]["windows"]
.as_array()
.expect("quota windows should exist");
let label_for_model = |model: &str| {
windows
.iter()
.filter_map(Value::as_object)
.find(|window| window.get("model") == Some(&json!(model)))
.and_then(|window| window.get("label"))
.cloned()
};
assert_eq!(
label_for_model("gemini-3.5-flash-extra-low"),
Some(json!("Gemini 3.5 Flash (Low)"))
);
assert_eq!(
label_for_model("gemini-3.5-flash-low"),
Some(json!("Gemini 3.5 Flash (Medium)"))
);
assert_eq!(
label_for_model("gemini-3-flash-agent"),
Some(json!("Gemini 3.5 Flash (High)"))
);
assert_eq!(
label_for_model("gemini-2.5-flash"),
Some(json!("Gemini 3.1 Flash Lite"))
);
assert_eq!(
label_for_model("claude-sonnet-4-6"),
Some(json!("Claude Sonnet 4.6 (Thinking)"))
);
}
#[test] #[test]
fn provider_key_status_snapshot_payload_backfills_account_block_from_oauth_invalid_reason() { fn provider_key_status_snapshot_payload_backfills_account_block_from_oauth_invalid_reason() {
let mut key = sample_catalog_key(); let mut key = sample_catalog_key();
@@ -1469,15 +1469,36 @@ async fn gateway_executes_openai_chat_cross_format_tool_use_upstream_stream_via_
} }
#[test] #[test]
fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported() { fn gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_finalize_response() {
run_openai_chat_finalize_test( run_openai_chat_finalize_test(
"gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported", "gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_finalize_response",
gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported_impl, gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_finalize_response_impl,
); );
} }
async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported_impl( async fn gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_finalize_response_impl(
) { ) {
use base64::Engine as _;
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
url: String,
accept: String,
authorization: String,
x_client_name: String,
x_client_version: String,
x_vscode_sessionid: String,
x_goog_api_client: String,
project: String,
request_id: String,
model: String,
user_agent: String,
request_type: String,
contents_len: usize,
request_has_model: bool,
}
fn hash_api_key(value: &str) -> String { fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(value.as_bytes()); hasher.update(value.as_bytes());
@@ -1582,11 +1603,11 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
) )
.expect("endpoint should build") .expect("endpoint should build")
.with_transport_fields( .with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(), "https://antigravity.googleapis.com".to_string(),
None, None,
None, None,
Some(2), Some(2),
Some("/v1beta/models/claude-sonnet-4-5:generateContent".to_string()), None,
None, None,
None, None,
None, None,
@@ -1595,6 +1616,11 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
} }
fn sample_provider_catalog_key() -> StoredProviderCatalogKey { fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
let encrypted_auth_config = encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
r#"{"provider_type":"antigravity","project_id":"project-antigravity-chat-local-1","client_version":"1.2.3","session_id":"sess-antigravity-chat-local-123","access_token_import_temporary":true,"headers":{"Authorization":"Bearer imported-antigravity-chat-token"}}"#,
)
.expect("auth config should encrypt");
StoredProviderCatalogKey::new( StoredProviderCatalogKey::new(
"key-openai-chat-antigravity-finalize-local-1".to_string(), "key-openai-chat-antigravity-finalize-local-1".to_string(),
"provider-openai-chat-antigravity-finalize-local-1".to_string(), "provider-openai-chat-antigravity-finalize-local-1".to_string(),
@@ -1606,12 +1632,9 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
.expect("key should build") .expect("key should build")
.with_transport_fields( .with_transport_fields(
Some(serde_json::json!(["gemini:generate_content"])), Some(serde_json::json!(["gemini:generate_content"])),
encrypt_python_fernet_plaintext( encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "__placeholder__")
DEVELOPMENT_ENCRYPTION_KEY, .expect("api key should encrypt"),
"sk-upstream-openai-chat-antigravity-finalize", Some(encrypted_auth_config),
)
.expect("api key should encrypt"),
None,
None, None,
Some(serde_json::json!({"gemini:generate_content": 1})), Some(serde_json::json!({"gemini:generate_content": 1})),
None, None,
@@ -1632,6 +1655,8 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
let plan_hits_clone = Arc::clone(&plan_hits); let plan_hits_clone = Arc::clone(&plan_hits);
let public_hits = Arc::new(Mutex::new(0usize)); let public_hits = Arc::new(Mutex::new(0usize));
let public_hits_clone = Arc::clone(&public_hits); let public_hits_clone = Arc::clone(&public_hits);
let seen_execution_runtime = Arc::new(Mutex::new(None::<SeenExecutionRuntimeSyncRequest>));
let seen_execution_runtime_clone = Arc::clone(&seen_execution_runtime);
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default()); let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let usage_repository = Arc::new(InMemoryUsageReadRepository::default()); let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
@@ -1692,42 +1717,130 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
let execution_runtime = Router::new().route( let execution_runtime = Router::new().route(
"/v1/execute/sync", "/v1/execute/sync",
any(|_request: Request| async move { any(move |request: Request| {
Json(json!({ let seen_execution_runtime_inner = Arc::clone(&seen_execution_runtime_clone);
"request_id": "trace-openai-chat-antigravity-sync-123", async move {
"status_code": 200, let (parts, body) = request.into_parts();
"headers": { let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
"content-type": "application/json" let payload: serde_json::Value =
}, serde_json::from_slice(&raw_body).expect("execution runtime payload should parse");
"body": { *seen_execution_runtime_inner.lock().expect("mutex should lock") =
"json_body": { Some(SeenExecutionRuntimeSyncRequest {
"response": { trace_id: parts
"responseId": "resp-antigravity-chat-sync-123", .headers
"candidates": [{ .get(TRACE_ID_HEADER)
"content": { .and_then(|value| value.to_str().ok())
"parts": [ .unwrap_or_default()
{"text": "Need a tool."}, .to_string(),
{"functionCall": {"name": "get_weather", "args": {"city": "SF"}}} url: payload
], .get("url")
"role": "model" .and_then(|value| value.as_str())
}, .unwrap_or_default()
"finishReason": "STOP", .to_string(),
"index": 0 accept: payload
}], .get("headers")
"modelVersion": "claude-sonnet-4-5", .and_then(|value| value.get("accept"))
"usageMetadata": { .and_then(|value| value.as_str())
"promptTokenCount": 2, .unwrap_or_default()
"candidatesTokenCount": 3, .to_string(),
"totalTokenCount": 5 authorization: payload
} .get("headers")
}, .and_then(|value| value.get("authorization"))
"responseId": "resp-antigravity-chat-sync-123" .and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
x_client_name: payload
.get("headers")
.and_then(|value| value.get("x-client-name"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
x_client_version: payload
.get("headers")
.and_then(|value| value.get("x-client-version"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
x_vscode_sessionid: payload
.get("headers")
.and_then(|value| value.get("x-vscode-sessionid"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
x_goog_api_client: payload
.get("headers")
.and_then(|value| value.get("x-goog-api-client"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
project: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("project"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
request_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("requestId"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_agent: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("userAgent"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
request_type: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("requestType"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
contents_len: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("contents"))
.and_then(|value| value.as_array())
.map(Vec::len)
.unwrap_or_default(),
request_has_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
});
Json(json!({
"request_id": "trace-openai-chat-antigravity-sync-123",
"status_code": 200,
"headers": {
"content-type": "text/event-stream"
},
"body": {
"body_bytes_b64": base64::engine::general_purpose::STANDARD.encode(
concat!(
"data: {\"response\":{\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Antigravity Chat\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"claude-sonnet-4-5\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}},\"responseId\":\"resp-antigravity-chat-sync-123\"}\n\n"
)
)
},
"telemetry": {
"elapsed_ms": 29
} }
}, }))
"telemetry": { }
"elapsed_ms": 29
}
}))
}), }),
); );
@@ -1779,31 +1892,84 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
.await .await
.expect("request should succeed"); .expect("request should succeed");
let elapsed = started_at.elapsed(); let elapsed = started_at.elapsed();
let response_status = response.status(); let response_status = response.status();
let execution_path = response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string();
let miss_reason = response
.headers()
.get(LOCAL_EXECUTION_RUNTIME_MISS_REASON_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string();
let response_body = response.text().await.expect("body should read"); let response_body = response.text().await.expect("body should read");
assert_eq!(response_status, StatusCode::OK);
let response_json: serde_json::Value =
serde_json::from_str(&response_body).expect("body should parse");
assert_eq!( assert_eq!(
response_status, response_json,
StatusCode::SERVICE_UNAVAILABLE, json!({
"unexpected gateway response: path={execution_path} miss={miss_reason} body={response_body}" "id": "resp-local-stream",
"object": "chat.completion",
"model": "claude-sonnet-4-5",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello Antigravity Chat"
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 2,
"completion_tokens": 3,
"total_tokens": 5
}
})
);
assert!(
elapsed < std::time::Duration::from_millis(3_500),
"response took unexpectedly long for local finalize path: elapsed={elapsed:?} finalize_hits={} report_hits={}",
*finalize_hits.lock().expect("mutex should lock"),
*report_hits.lock().expect("mutex should lock"),
);
let seen_execution_runtime_request = seen_execution_runtime
.lock()
.expect("mutex should lock")
.clone()
.expect("execution runtime request should be captured");
assert_eq!(
seen_execution_runtime_request.trace_id,
"trace-openai-chat-antigravity-sync-123"
); );
assert_eq!( assert_eq!(
execution_path, "local_execution_runtime_miss", seen_execution_runtime_request.url,
"unexpected execution path: miss={miss_reason} body={response_body}" "https://antigravity.googleapis.com/v1internal:streamGenerateContent?alt=sse"
); );
assert_eq!(miss_reason, "all_candidates_skipped"); assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
assert_eq!(
seen_execution_runtime_request.authorization,
"Bearer imported-antigravity-chat-token"
);
assert_eq!(seen_execution_runtime_request.x_client_name, "antigravity");
assert_eq!(seen_execution_runtime_request.x_client_version, "1.2.3");
assert_eq!(
seen_execution_runtime_request.x_vscode_sessionid,
"sess-antigravity-chat-local-123"
);
assert_eq!(
seen_execution_runtime_request.x_goog_api_client,
"gl-node/18.18.2 fire/0.8.6 grpc/1.10.x"
);
assert_eq!(
seen_execution_runtime_request.project,
"project-antigravity-chat-local-1"
);
assert_eq!(
seen_execution_runtime_request.request_id,
"trace-openai-chat-antigravity-sync-123"
);
assert_eq!(seen_execution_runtime_request.model, "claude-sonnet-4-5");
assert_eq!(
seen_execution_runtime_request.user_agent,
aether_provider_transport::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT
);
assert_eq!(seen_execution_runtime_request.request_type, "agent");
assert_eq!(seen_execution_runtime_request.contents_len, 1);
assert!(!seen_execution_runtime_request.request_has_model);
let mut stored_candidates = Vec::new(); let mut stored_candidates = Vec::new();
for _ in 0..50 { for _ in 0..50 {
@@ -1819,11 +1985,8 @@ async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_tr
tokio::time::sleep(std::time::Duration::from_millis(10)).await; tokio::time::sleep(std::time::Duration::from_millis(10)).await;
} }
assert_eq!(stored_candidates.len(), 1); assert_eq!(stored_candidates.len(), 1);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Skipped); assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Success);
assert_eq!( assert_eq!(stored_candidates[0].skip_reason.as_deref(), None);
stored_candidates[0].skip_reason.as_deref(),
Some("transport_provider_type_unsupported")
);
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert_eq!(*report_hits.lock().expect("mutex should lock"), 0); assert_eq!(*report_hits.lock().expect("mutex should lock"), 0);
assert_eq!(*finalize_hits.lock().expect("mutex should lock"), 0); assert_eq!(*finalize_hits.lock().expect("mutex should lock"), 0);
@@ -1520,7 +1520,10 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
seen_remote_execution_runtime_request.url, seen_remote_execution_runtime_request.url,
"https://antigravity.googleapis.com/v1internal:streamGenerateContent?alt=sse" "https://antigravity.googleapis.com/v1internal:streamGenerateContent?alt=sse"
); );
assert_eq!(seen_remote_execution_runtime_request.accept, "*/*"); assert_eq!(
seen_remote_execution_runtime_request.accept,
"text/event-stream"
);
assert_eq!( assert_eq!(
seen_remote_execution_runtime_request.authorization, seen_remote_execution_runtime_request.authorization,
"Bearer refreshed-antigravity-cli-access-token" "Bearer refreshed-antigravity-cli-access-token"
@@ -1555,7 +1558,7 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
); );
assert_eq!( assert_eq!(
seen_remote_execution_runtime_request.user_agent, seen_remote_execution_runtime_request.user_agent,
"antigravity" aether_provider_transport::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT
); );
assert_eq!(seen_remote_execution_runtime_request.request_type, "agent"); assert_eq!(seen_remote_execution_runtime_request.request_type, "agent");
assert_eq!(seen_remote_execution_runtime_request.contents_len, 1); assert_eq!(seen_remote_execution_runtime_request.contents_len, 1);
@@ -2143,7 +2143,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_upstream_stream_via_local_
); );
assert_eq!( assert_eq!(
seen_remote_execution_runtime_request.user_agent, seen_remote_execution_runtime_request.user_agent,
"antigravity" aether_provider_transport::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT
); );
assert_eq!(seen_remote_execution_runtime_request.request_type, "agent"); assert_eq!(seen_remote_execution_runtime_request.request_type, "agent");
assert_eq!(seen_remote_execution_runtime_request.contents_len, 0); assert_eq!(seen_remote_execution_runtime_request.contents_len, 0);
@@ -2031,7 +2031,10 @@ async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_
"trace-antigravity-cli-oauth-local-stream-123" "trace-antigravity-cli-oauth-local-stream-123"
); );
assert_eq!(seen_execution_runtime_request.model, "claude-sonnet-4-5"); assert_eq!(seen_execution_runtime_request.model, "claude-sonnet-4-5");
assert_eq!(seen_execution_runtime_request.user_agent, "antigravity"); assert_eq!(
seen_execution_runtime_request.user_agent,
aether_provider_transport::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT
);
assert_eq!(seen_execution_runtime_request.request_type, "agent"); assert_eq!(seen_execution_runtime_request.request_type, "agent");
assert_eq!(seen_execution_runtime_request.contents_len, 0); assert_eq!(seen_execution_runtime_request.contents_len, 0);
assert!((seen_execution_runtime_request.exact_temperature - 0.2).abs() < f64::EPSILON); assert!((seen_execution_runtime_request.exact_temperature - 0.2).abs() < f64::EPSILON);
@@ -2334,7 +2334,10 @@ async fn gateway_executes_antigravity_gemini_cli_sync_via_local_decision_gate_af
"trace-antigravity-cli-oauth-local-sync-123" "trace-antigravity-cli-oauth-local-sync-123"
); );
assert_eq!(seen_execution_runtime_request.model, "claude-sonnet-4-5"); assert_eq!(seen_execution_runtime_request.model, "claude-sonnet-4-5");
assert_eq!(seen_execution_runtime_request.user_agent, "antigravity"); assert_eq!(
seen_execution_runtime_request.user_agent,
aether_provider_transport::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT
);
assert_eq!(seen_execution_runtime_request.request_type, "agent"); assert_eq!(seen_execution_runtime_request.request_type, "agent");
assert_eq!(seen_execution_runtime_request.contents_len, 0); assert_eq!(seen_execution_runtime_request.contents_len, 0);
assert!((seen_execution_runtime_request.exact_temperature - 0.2).abs() < f64::EPSILON); assert!((seen_execution_runtime_request.exact_temperature - 0.2).abs() < f64::EPSILON);
@@ -23,6 +23,30 @@ use crate::constants::{
}; };
use crate::data::GatewayDataState; use crate::data::GatewayDataState;
const PROVIDER_QUOTA_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_provider_quota_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(PROVIDER_QUOTA_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("provider quota test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[tokio::test] #[tokio::test]
async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_admin_principal() { async fn gateway_refreshes_admin_provider_quota_locally_for_codex_with_trusted_admin_principal() {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -1748,8 +1772,15 @@ async fn gateway_reports_codex_quota_runtime_failures_locally_without_falling_ba
upstream_handle.abort(); upstream_handle.abort();
} }
#[tokio::test] #[test]
async fn gateway_refreshes_admin_provider_quota_locally_for_antigravity_with_trusted_admin_principal( fn gateway_refreshes_admin_provider_quota_locally_for_antigravity_with_trusted_admin_principal() {
run_provider_quota_test(
"gateway_refreshes_admin_provider_quota_locally_for_antigravity_with_trusted_admin_principal",
gateway_refreshes_admin_provider_quota_locally_for_antigravity_with_trusted_admin_principal_inner,
);
}
async fn gateway_refreshes_admin_provider_quota_locally_for_antigravity_with_trusted_admin_principal_inner(
) { ) {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct SeenExecutionRuntimeRequest { struct SeenExecutionRuntimeRequest {
@@ -4125,7 +4125,7 @@ async fn gateway_reports_transport_unsupported_reason_for_non_kiro_provider_impl
assert_eq!( assert_eq!(
payload["error"], payload["error"],
json!( json!(
"Rust local provider-query model test cannot execute endpoint format gemini:generate_content (transport_antigravity_auth_unsupported)" "Rust local provider-query model test cannot execute endpoint format gemini:generate_content (transport_antigravity_auth_config_missing)"
) )
); );
@@ -4305,6 +4305,204 @@ async fn gateway_handles_antigravity_endpoint_test_model_locally_impl() {
execution_runtime_handle.abort(); execution_runtime_handle.abort();
} }
#[test]
fn gateway_hydrates_antigravity_project_id_from_load_code_assist_for_test_model() {
run_provider_query_test(
"gateway_hydrates_antigravity_project_id_from_load_code_assist_for_test_model",
gateway_hydrates_antigravity_project_id_from_load_code_assist_for_test_model_impl,
);
}
async fn gateway_hydrates_antigravity_project_id_from_load_code_assist_for_test_model_impl() {
let seen_urls = Arc::new(Mutex::new(Vec::<String>::new()));
let seen_urls_clone = Arc::clone(&seen_urls);
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| {
let seen_urls_inner = Arc::clone(&seen_urls_clone);
async move {
seen_urls_inner
.lock()
.expect("mutex should lock")
.push(plan.url.clone());
if plan.url == "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
{
assert_eq!(plan.model_name.as_deref(), Some("loadCodeAssist"));
assert_eq!(
plan.headers.get("authorization").map(String::as_str),
Some("Bearer cached-antigravity-token")
);
assert_eq!(
plan.body.json_body.as_ref().and_then(|body| body
.get("metadata")
.and_then(|metadata| metadata.get("pluginType"))),
Some(&json!("GEMINI"))
);
return Json(json!({
"request_id": plan.request_id,
"candidate_id": plan.candidate_id,
"status_code": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {
"cloudaicompanionProject": {
"id": "project-from-antigravity-load-code-assist"
},
"currentTier": {
"id": "free"
}
}
}
}));
}
assert_eq!(
plan.url,
"https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(plan.provider_id, "provider-antigravity");
assert_eq!(plan.endpoint_id, "endpoint-antigravity-gemini");
assert_eq!(plan.key_id, "key-antigravity-gemini");
assert_eq!(plan.provider_api_format, "gemini:generate_content");
assert!(!plan.stream);
assert_eq!(
plan.body.json_body.as_ref().unwrap()["project"],
json!("project-from-antigravity-load-code-assist")
);
assert_eq!(
plan.body.json_body.as_ref().unwrap()["requestType"],
json!("endpoint_test")
);
assert_eq!(
plan.body.json_body.as_ref().unwrap()["model"],
json!("gemini-3.1-flash-lite")
);
Json(json!({
"request_id": plan.request_id,
"candidate_id": plan.candidate_id,
"status_code": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"json_body": {
"response": {
"candidates": [{
"content": {
"parts": [
{"text": "Hello from hydrated Antigravity"}
],
"role": "model"
},
"finishReason": "STOP",
"index": 0
}],
"modelVersion": "gemini-3.1-flash-lite",
"usageMetadata": {
"promptTokenCount": 2,
"candidatesTokenCount": 4,
"totalTokenCount": 6
}
},
"responseId": "resp-antigravity-hydrated-test-123"
}
},
"telemetry": {
"elapsed_ms": 23
}
}))
}
}),
);
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
let mut provider = sample_provider("provider-antigravity", "Antigravity", 10);
provider.provider_type = "antigravity".to_string();
let mut key = sample_key(
"key-antigravity-gemini",
"provider-antigravity",
"gemini:generate_content",
"cached-antigravity-token",
);
key.auth_type = "oauth".to_string();
key.encrypted_auth_config = Some(
aether_crypto::encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
r#"{"provider_type":"antigravity","refresh_token":"rt-antigravity-123"}"#,
)
.expect("auth config should encrypt"),
);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![provider],
vec![sample_endpoint(
"endpoint-antigravity-gemini",
"provider-antigravity",
"gemini:generate_content",
"https://daily-cloudcode-pa.googleapis.com",
)],
vec![key],
));
let gateway = build_router_with_state(
build_state_with_execution_runtime_override(execution_runtime_url)
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_repository_for_tests(Arc::clone(
&provider_catalog_repository,
))
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
),
);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/provider-query/test-model"))
.header(GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user-123")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
.json(&json!({
"provider_id": "provider-antigravity",
"model": "gemini-3.1-flash-lite",
"api_format": "gemini:generate_content",
"message": "Say hello"
}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("json body should parse");
assert_eq!(payload["success"], json!(true));
assert_eq!(
payload["data"]["response"]["choices"][0]["message"]["content"],
json!("Hello from hydrated Antigravity")
);
assert_eq!(
*seen_urls.lock().expect("mutex should lock"),
vec![
"https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist".to_string(),
"https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent".to_string(),
]
);
let reloaded = provider_catalog_repository
.list_keys_by_ids(&["key-antigravity-gemini".to_string()])
.await
.expect("key should reload");
assert_eq!(
reloaded[0]
.upstream_metadata
.as_ref()
.and_then(|metadata| metadata.get("antigravity"))
.and_then(|metadata| metadata.get("project_id")),
Some(&json!("project-from-antigravity-load-code-assist"))
);
gateway_handle.abort();
execution_runtime_handle.abort();
}
#[test] #[test]
fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted() { fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted() {
run_provider_query_test( run_provider_query_test(
@@ -1452,6 +1452,11 @@ async fn gateway_handles_admin_system_api_formats_locally_with_trusted_admin_pri
assert!(formats.iter().any(|item| item["value"] == "jina:embedding")); assert!(formats.iter().any(|item| item["value"] == "jina:embedding"));
assert!(formats.iter().any(|item| item["value"] == "jina:rerank")); assert!(formats.iter().any(|item| item["value"] == "jina:rerank"));
assert!(formats.iter().any(|item| item["value"] == "gemini:video")); assert!(formats.iter().any(|item| item["value"] == "gemini:video"));
let gemini_interactions = formats
.iter()
.find(|item| item["value"] == "gemini:interactions")
.expect("gemini interactions format should exist");
assert_eq!(gemini_interactions["default_path"], "/v1/interactions");
let aliyun_embedding = formats let aliyun_embedding = formats
.iter() .iter()
.find(|item| item["value"] == "aliyun:multimodal_embedding") .find(|item| item["value"] == "aliyun:multimodal_embedding")
+73 -6
View File
@@ -949,6 +949,10 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
"/v1internal:fetchAvailableModels", "/v1internal:fetchAvailableModels",
json!({"project": "aether-antigravity-local"}), json!({"project": "aether-antigravity-local"}),
), ),
(
"/v1internal:retrieveUserQuotaSummary",
json!({"project": "aether-antigravity-local"}),
),
( (
"/v1internal:fetchUserInfo", "/v1internal:fetchUserInfo",
json!({"project": "aether-antigravity-local"}), json!({"project": "aether-antigravity-local"}),
@@ -966,6 +970,10 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
"metrics": [] "metrics": []
}), }),
), ),
(
"/v1internal:writeTrajectoryAcls",
json!({"trajectoryId": "trajectory-ant-123"}),
),
( (
"/v1internal:setUserSettings", "/v1internal:setUserSettings",
json!({"userSettings": user_settings.clone()}), json!({"userSettings": user_settings.clone()}),
@@ -1012,14 +1020,58 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
); );
} }
"/v1internal:fetchAvailableModels" => { "/v1internal:fetchAvailableModels" => {
assert_eq!(payload["defaultAgentModelId"], "gemini-3.1-flash-lite"); assert_eq!(payload["defaultAgentModelId"], "gemini-3.5-flash-low");
assert_eq!( assert_eq!(
payload["tieredModelIds"]["flash"], payload["tieredModelIds"]["flash"],
json!(["gemini-3-flash-agent"]) json!(["gemini-3-flash-agent"])
); );
assert_eq!(
payload["tieredModelIds"]["pro"],
json!(["gemini-3.1-pro-low"])
);
assert_eq!(
payload["models"]["gemini-3-flash-agent"]["displayName"],
"Gemini 3.5 Flash (High)"
);
assert_eq!( assert_eq!(
payload["models"]["gemini-3.5-flash-low"]["displayName"], payload["models"]["gemini-3.5-flash-low"]["displayName"],
"Gemini 3.5 Flash Low" "Gemini 3.5 Flash (Medium)"
);
assert_eq!(
payload["models"]["gemini-3.5-flash-extra-low"]["displayName"],
"Gemini 3.5 Flash (Low)"
);
assert_eq!(
payload["models"]["gemini-pro-agent"]["displayName"],
"Gemini 3.1 Pro (High)"
);
assert_eq!(
payload["models"]["claude-opus-4-6-thinking"]["displayName"],
"Claude Opus 4.6 (Thinking)"
);
assert_eq!(
payload["models"]["gpt-oss-120b-medium"]["displayName"],
"GPT-OSS 120B (Medium)"
);
assert_eq!(
payload["models"]["gemini-pro-agent"]["model"],
"MODEL_PLACEHOLDER_M16"
);
assert_eq!(
payload["models"]["gemini-3.1-pro-high"]["model"],
"MODEL_PLACEHOLDER_M37"
);
assert_eq!(
payload["models"]["gemini-3.5-flash-extra-low"]["model"],
"MODEL_PLACEHOLDER_M187"
);
assert_eq!(
payload["models"]["claude-sonnet-4-6"]["apiProvider"],
"API_PROVIDER_ANTHROPIC_VERTEX"
);
assert_eq!(
payload["models"]["gpt-oss-120b-medium"]["apiProvider"],
"API_PROVIDER_OPENAI_VERTEX"
); );
assert_eq!( assert_eq!(
payload["models"]["gemini-3.5-flash-low"]["apiProvider"], payload["models"]["gemini-3.5-flash-low"]["apiProvider"],
@@ -1032,18 +1084,26 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
assert_eq!( assert_eq!(
payload["agentModelSorts"][0]["groups"][0]["modelIds"], payload["agentModelSorts"][0]["groups"][0]["modelIds"],
json!([ json!([
"gemini-3.1-flash-lite", "gemini-3.5-flash-low",
"gemini-3-flash-agent", "gemini-3-flash-agent",
"gemini-3.5-flash-extra-low",
"gemini-3.1-pro-low", "gemini-3.1-pro-low",
"gemini-3.5-flash-low" "gemini-pro-agent",
"claude-sonnet-4-6",
"claude-opus-4-6-thinking",
"gpt-oss-120b-medium"
]) ])
); );
assert_eq!(payload["deprecatedModelIds"], json!({})); assert_eq!(
payload["deprecatedModelIds"]["gemini-3.1-pro-high"]["newModelId"],
"gemini-pro-agent"
);
assert_eq!(payload["commandModelIds"], json!(["gemini-3-flash"])); assert_eq!(payload["commandModelIds"], json!(["gemini-3-flash"]));
assert_eq!( assert_eq!(
payload["imageGenerationModelIds"], payload["imageGenerationModelIds"],
json!(["gemini-3.1-flash-image"]) json!(["gemini-3.1-flash-image"])
); );
assert_eq!(payload["tabModelIds"], json!(["chat_20706", "chat_23310"]));
assert_eq!(payload["mqueryModelIds"], json!(["gemini-3.1-flash-lite"])); assert_eq!(payload["mqueryModelIds"], json!(["gemini-3.1-flash-lite"]));
assert_eq!( assert_eq!(
payload["webSearchModelIds"], payload["webSearchModelIds"],
@@ -1058,9 +1118,13 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
assert_eq!(payload["regionCode"], "US"); assert_eq!(payload["regionCode"], "US");
assert_eq!( assert_eq!(
payload["userSettings"]["preferredModelId"], payload["userSettings"]["preferredModelId"],
"gemini-3.1-flash-lite" "gemini-3.5-flash-low"
); );
} }
"/v1internal:retrieveUserQuotaSummary" => {
assert_eq!(payload["description"], "");
assert_eq!(payload["groups"], json!([]));
}
"/v1internal:fetchAdminControls" => { "/v1internal:fetchAdminControls" => {
assert_eq!(payload, json!({})); assert_eq!(payload, json!({}));
} }
@@ -1071,6 +1135,9 @@ async fn gateway_handles_antigravity_v1internal_control_plane_without_proxying()
"/v1internal:recordCodeAssistMetrics" => { "/v1internal:recordCodeAssistMetrics" => {
assert_eq!(payload, json!({})); assert_eq!(payload, json!({}));
} }
"/v1internal:writeTrajectoryAcls" => {
assert_eq!(payload, json!({}));
}
"/v1internal:setUserSettings" => { "/v1internal:setUserSettings" => {
assert_eq!(payload["userSettings"], user_settings); assert_eq!(payload["userSettings"], user_settings);
} }
+116 -4
View File
@@ -223,13 +223,19 @@ pub fn parse_antigravity_usage_response(
) -> Option<serde_json::Value> { ) -> Option<serde_json::Value> {
let models = value.get("models")?.as_object()?; let models = value.get("models")?.as_object()?;
let mut quota_by_model = serde_json::Map::new(); let mut quota_by_model = serde_json::Map::new();
let mut opaque_key_display_index = 1usize;
for (model_id, model_value) in models { for (model_id, model_value) in models {
let mut payload = serde_json::Map::new(); let mut payload = serde_json::Map::new();
if let Some(display_name) = coerce_json_string( let upstream_display_name = coerce_json_string(
model_value model_value
.get("displayName") .get("displayName")
.or_else(|| model_value.get("display_name")), .or_else(|| model_value.get("display_name")),
);
if let Some(display_name) = friendly_quota_display_name(
upstream_display_name,
model_id,
&mut opaque_key_display_index,
) { ) {
payload.insert("display_name".to_string(), json!(display_name)); payload.insert("display_name".to_string(), json!(display_name));
} }
@@ -270,6 +276,7 @@ pub fn parse_gemini_cli_retrieve_user_quota_response(
) -> Option<serde_json::Value> { ) -> Option<serde_json::Value> {
let buckets = value.get("buckets")?.as_array()?; let buckets = value.get("buckets")?.as_array()?;
let mut quota_by_model = serde_json::Map::new(); let mut quota_by_model = serde_json::Map::new();
let mut opaque_key_display_index = 1usize;
for bucket in buckets { for bucket in buckets {
if !bucket.is_object() { if !bucket.is_object() {
@@ -320,6 +327,12 @@ pub fn parse_gemini_cli_retrieve_user_quota_response(
.or_else(|| model_id.clone()) .or_else(|| model_id.clone())
.or_else(|| token_type.clone()) .or_else(|| token_type.clone())
.unwrap_or_else(|| quota_key.clone()); .unwrap_or_else(|| quota_key.clone());
let display_name = friendly_quota_display_name(
Some(display_name),
&quota_key,
&mut opaque_key_display_index,
)
.unwrap_or_else(|| quota_key.clone());
let remaining_fraction = first_json_f64_by_paths( let remaining_fraction = first_json_f64_by_paths(
bucket, bucket,
&[ &[
@@ -456,6 +469,38 @@ pub fn parse_gemini_cli_retrieve_user_quota_response(
})) }))
} }
fn is_opaque_reset_credit_quota_identifier(value: &str) -> bool {
value.trim().starts_with("RateLimitResetCredit_")
}
fn friendly_quota_display_name(
candidate: Option<String>,
quota_key: &str,
opaque_key_display_index: &mut usize,
) -> Option<String> {
let candidate = candidate
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
if let Some(candidate) = candidate.as_deref() {
if !is_opaque_reset_credit_quota_identifier(candidate) {
return Some(candidate.to_string());
}
}
if is_opaque_reset_credit_quota_identifier(quota_key)
|| candidate
.as_deref()
.is_some_and(is_opaque_reset_credit_quota_identifier)
{
let label = format!("Key-{}", *opaque_key_display_index);
*opaque_key_display_index += 1;
return Some(label);
}
candidate
}
pub fn parse_gemini_cli_v1internal_credits_response( pub fn parse_gemini_cli_v1internal_credits_response(
value: &serde_json::Value, value: &serde_json::Value,
updated_at_unix_secs: u64, updated_at_unix_secs: u64,
@@ -1999,9 +2044,10 @@ pub fn parse_chatgpt_web_conversation_init_response(
mod tests { mod tests {
use super::{ use super::{
codex_build_invalid_state, codex_runtime_invalid_reason, codex_build_invalid_state, codex_runtime_invalid_reason,
normalize_codex_reset_credit_consume_outcome, parse_chatgpt_web_conversation_init_response, normalize_codex_reset_credit_consume_outcome, parse_antigravity_usage_response,
parse_codex_backend_me_response, parse_codex_wham_reset_credits_detail_response, parse_chatgpt_web_conversation_init_response, parse_codex_backend_me_response,
parse_codex_wham_usage_response, parse_gemini_cli_retrieve_user_quota_response, parse_codex_wham_reset_credits_detail_response, parse_codex_wham_usage_response,
parse_gemini_cli_retrieve_user_quota_response,
parse_gemini_cli_v1internal_credits_response, parse_windsurf_model_configs_response, parse_gemini_cli_v1internal_credits_response, parse_windsurf_model_configs_response,
parse_windsurf_rate_limit_response, parse_windsurf_user_status_response, parse_windsurf_rate_limit_response, parse_windsurf_user_status_response,
provider_auto_remove_quota_exhausted_keys, quota_refresh_success_invalid_state, provider_auto_remove_quota_exhausted_keys, quota_refresh_success_invalid_state,
@@ -2538,6 +2584,40 @@ mod tests {
assert!(parsed.get("secondary_used_percent").is_none()); assert!(parsed.get("secondary_used_percent").is_none());
} }
#[test]
fn parses_antigravity_usage_response_labels_opaque_reset_credit_keys() {
let parsed = parse_antigravity_usage_response(
&json!({
"models": {
"RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb": {
"quotaInfo": {
"remainingFraction": 0.75,
"resetTime": "2030-01-01T00:00:00Z"
}
},
"gemini-3-pro-preview": {
"displayName": "Gemini 3 Pro Preview",
"quotaInfo": {
"remainingFraction": 0.25
}
}
}
}),
1_777_000_000,
)
.expect("antigravity quota should parse");
assert_eq!(
parsed["models"]["RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb"]
["display_name"],
json!("Key-1")
);
assert_eq!(
parsed["models"]["gemini-3-pro-preview"]["display_name"],
json!("Gemini 3 Pro Preview")
);
}
#[test] #[test]
fn parses_gemini_cli_retrieve_user_quota_buckets() { fn parses_gemini_cli_retrieve_user_quota_buckets() {
let parsed = parse_gemini_cli_retrieve_user_quota_response( let parsed = parse_gemini_cli_retrieve_user_quota_response(
@@ -2595,6 +2675,38 @@ mod tests {
); );
} }
#[test]
fn parses_gemini_cli_quota_buckets_labels_opaque_reset_credit_keys() {
let parsed = parse_gemini_cli_retrieve_user_quota_response(
&json!({
"buckets": [
{
"tokenType": "RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb",
"remainingFraction": 0.5
},
{
"modelId": "RateLimitResetCredit_d18b8aac4ec2472697ad747a14975ac8",
"displayName": "RateLimitResetCredit_d18b8aac4ec2472697ad747a14975ac8",
"remainingFraction": 0.25
}
]
}),
1_777_000_000,
)
.expect("gemini cli quota should parse");
assert_eq!(
parsed["quota_by_model"]["RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb"]
["display_name"],
json!("Key-1")
);
assert_eq!(
parsed["quota_by_model"]["RateLimitResetCredit_d18b8aac4ec2472697ad747a14975ac8"]
["display_name"],
json!("Key-2")
);
}
#[test] #[test]
fn parses_gemini_cli_v1internal_credits() { fn parses_gemini_cli_v1internal_credits() {
let parsed = parse_gemini_cli_v1internal_credits_response( let parsed = parse_gemini_cli_v1internal_credits_response(
+6
View File
@@ -805,6 +805,12 @@ const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
default_path: "/v1beta/models/{model}:{action}", default_path: "/v1beta/models/{model}:{action}",
aliases: &["gemini", "google", "vertex"], aliases: &["gemini", "google", "vertex"],
}, },
AdminApiFormatDefinition {
value: "gemini:interactions",
label: "Gemini Interactions",
default_path: "/v1/interactions",
aliases: &["gemini_interactions", "interactions"],
},
AdminApiFormatDefinition { AdminApiFormatDefinition {
value: "gemini:embedding", value: "gemini:embedding",
label: "Gemini Embedding", label: "Gemini Embedding",
+14 -10
View File
@@ -19,7 +19,10 @@ pub use crate::contracts::{
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_FILES_DELETE_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND,
GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND,
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
@@ -256,14 +259,15 @@ pub use aether_ai_formats::{
from_claude_to_canonical_response, from_gemini_to_canonical_request, from_claude_to_canonical_response, from_gemini_to_canonical_request,
from_gemini_to_canonical_response, from_openai_chat_to_canonical_request, from_gemini_to_canonical_response, from_openai_chat_to_canonical_request,
from_openai_chat_to_canonical_response, from_openai_responses_to_canonical_request, from_openai_chat_to_canonical_response, from_openai_responses_to_canonical_request,
from_openai_responses_to_canonical_response, parse_request_pure, parse_response_pure, from_openai_responses_to_canonical_response, is_gemini_interactions_api_format,
request_candidate_api_format_preference, request_candidate_api_formats, parse_request_pure, parse_response_pure, request_candidate_api_format_preference,
request_conversion_kind, request_conversion_requires_enable_flag, request_candidate_api_formats, request_conversion_kind,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind, CanonicalContentBlock, request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
CanonicalGenerationConfig, CanonicalInstruction, CanonicalMessage, CanonicalRequest, sync_cli_response_conversion_kind, CanonicalContentBlock, CanonicalGenerationConfig,
CanonicalResponse, CanonicalResponseFormat, CanonicalResponseOutput, CanonicalRole, CanonicalInstruction, CanonicalMessage, CanonicalRequest, CanonicalResponse,
CanonicalStopReason, CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition, CanonicalResponseFormat, CanonicalResponseOutput, CanonicalRole, CanonicalStopReason,
CanonicalUsage, ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition, CanonicalUsage,
FormatContext, FormatError, FormatFamily, FormatId, FormatProfile, RequestConversionKind, ConversionFieldRecord, ConversionFieldStatus, ConversionReport, Converted, FormatContext,
FormatError, FormatFamily, FormatId, FormatProfile, RequestConversionKind,
SyncChatResponseConversionKind, SyncCliResponseConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
}; };
+12 -8
View File
@@ -16,14 +16,16 @@ pub use plan_kinds::{
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
}; };
pub use report_kinds::{ pub use report_kinds::{
core_error_background_report_kind, core_error_default_client_api_format, core_error_background_report_kind, core_error_default_client_api_format,
@@ -37,6 +39,8 @@ pub use report_kinds::{
GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND, GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND,
GEMINI_CLI_SYNC_ERROR_REPORT_KIND, GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND, GEMINI_CLI_SYNC_ERROR_REPORT_KIND, GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND,
GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND,
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND, GEMINI_VIDEO_CREATE_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND,
OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND, OPENAI_CHAT_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND, OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_ERROR_REPORT_KIND,
@@ -14,6 +14,7 @@ pub const GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND: &str = "gemini_video_cancel_sync";
pub const OPENAI_CHAT_STREAM_PLAN_KIND: &str = "openai_chat_stream"; pub const OPENAI_CHAT_STREAM_PLAN_KIND: &str = "openai_chat_stream";
pub const CLAUDE_CHAT_STREAM_PLAN_KIND: &str = "claude_chat_stream"; pub const CLAUDE_CHAT_STREAM_PLAN_KIND: &str = "claude_chat_stream";
pub const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream"; pub const GEMINI_CHAT_STREAM_PLAN_KIND: &str = "gemini_chat_stream";
pub const GEMINI_INTERACTIONS_STREAM_PLAN_KIND: &str = "gemini_interactions_stream";
pub const OPENAI_RESPONSES_STREAM_PLAN_KIND: &str = "openai_responses_stream"; pub const OPENAI_RESPONSES_STREAM_PLAN_KIND: &str = "openai_responses_stream";
pub const OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND: &str = "openai_responses_compact_stream"; pub const OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND: &str = "openai_responses_compact_stream";
pub const CLAUDE_CLI_STREAM_PLAN_KIND: &str = "claude_cli_stream"; pub const CLAUDE_CLI_STREAM_PLAN_KIND: &str = "claude_cli_stream";
@@ -27,6 +28,7 @@ pub const OPENAI_RESPONSES_SYNC_PLAN_KIND: &str = "openai_responses_sync";
pub const OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND: &str = "openai_responses_compact_sync"; pub const OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND: &str = "openai_responses_compact_sync";
pub const CLAUDE_CHAT_SYNC_PLAN_KIND: &str = "claude_chat_sync"; pub const CLAUDE_CHAT_SYNC_PLAN_KIND: &str = "claude_chat_sync";
pub const GEMINI_CHAT_SYNC_PLAN_KIND: &str = "gemini_chat_sync"; pub const GEMINI_CHAT_SYNC_PLAN_KIND: &str = "gemini_chat_sync";
pub const GEMINI_INTERACTIONS_SYNC_PLAN_KIND: &str = "gemini_interactions_sync";
pub const CLAUDE_CLI_SYNC_PLAN_KIND: &str = "claude_cli_sync"; pub const CLAUDE_CLI_SYNC_PLAN_KIND: &str = "claude_cli_sync";
pub const GEMINI_CLI_SYNC_PLAN_KIND: &str = "gemini_cli_sync"; pub const GEMINI_CLI_SYNC_PLAN_KIND: &str = "gemini_cli_sync";
@@ -1,13 +1,14 @@
use crate::contracts::{ use crate::contracts::{
CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
}; };
pub const OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_chat_sync_finalize"; pub const OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_chat_sync_finalize";
pub const CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "claude_chat_sync_finalize"; pub const CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "claude_chat_sync_finalize";
pub const GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_chat_sync_finalize"; pub const GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_chat_sync_finalize";
pub const GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND: &str = "gemini_interactions_sync_finalize";
pub const OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND: &str = "openai_responses_sync_finalize"; pub const OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND: &str = "openai_responses_sync_finalize";
pub const OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str = pub const OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str =
"openai_responses_compact_sync_finalize"; "openai_responses_compact_sync_finalize";
@@ -23,6 +24,7 @@ const LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND: &str = "openai_compact_sy
pub const OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "openai_chat_sync_success"; pub const OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "openai_chat_sync_success";
pub const CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "claude_chat_sync_success"; pub const CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "claude_chat_sync_success";
pub const GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_chat_sync_success"; pub const GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_chat_sync_success";
pub const GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_interactions_sync_success";
pub const OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND: &str = "openai_responses_sync_success"; pub const OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND: &str = "openai_responses_sync_success";
pub const OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND: &str = pub const OPENAI_RESPONSES_COMPACT_SYNC_SUCCESS_REPORT_KIND: &str =
"openai_responses_compact_sync_success"; "openai_responses_compact_sync_success";
@@ -35,6 +37,8 @@ pub const GEMINI_CLI_SYNC_SUCCESS_REPORT_KIND: &str = "gemini_cli_sync_success";
pub const OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "openai_chat_stream_success"; pub const OPENAI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "openai_chat_stream_success";
pub const CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "claude_chat_stream_success"; pub const CLAUDE_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "claude_chat_stream_success";
pub const GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_chat_stream_success"; pub const GEMINI_CHAT_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_chat_stream_success";
pub const GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND: &str =
"gemini_interactions_stream_success";
pub const OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND: &str = "openai_responses_stream_success"; pub const OPENAI_RESPONSES_STREAM_SUCCESS_REPORT_KIND: &str = "openai_responses_stream_success";
pub const OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND: &str = pub const OPENAI_RESPONSES_COMPACT_STREAM_SUCCESS_REPORT_KIND: &str =
"openai_responses_compact_stream_success"; "openai_responses_compact_stream_success";
@@ -45,6 +49,7 @@ pub const GEMINI_CLI_STREAM_SUCCESS_REPORT_KIND: &str = "gemini_cli_stream_succe
pub const OPENAI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "openai_chat_sync_error"; pub const OPENAI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "openai_chat_sync_error";
pub const CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND: &str = "claude_chat_sync_error"; pub const CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND: &str = "claude_chat_sync_error";
pub const GEMINI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "gemini_chat_sync_error"; pub const GEMINI_CHAT_SYNC_ERROR_REPORT_KIND: &str = "gemini_chat_sync_error";
pub const GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND: &str = "gemini_interactions_sync_error";
pub const OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND: &str = "openai_responses_sync_error"; pub const OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND: &str = "openai_responses_sync_error";
pub const OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND: &str = pub const OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND: &str =
"openai_responses_compact_sync_error"; "openai_responses_compact_sync_error";
@@ -58,6 +63,7 @@ pub fn implicit_sync_finalize_report_kind(plan_kind: &str) -> Option<&'static st
OPENAI_CHAT_SYNC_PLAN_KIND => Some(OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND), OPENAI_CHAT_SYNC_PLAN_KIND => Some(OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND),
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND), CLAUDE_CHAT_SYNC_PLAN_KIND => Some(CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND),
GEMINI_CHAT_SYNC_PLAN_KIND => Some(GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND), GEMINI_CHAT_SYNC_PLAN_KIND => Some(GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_PLAN_KIND => Some(GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND),
OPENAI_RESPONSES_SYNC_PLAN_KIND => Some(OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND), OPENAI_RESPONSES_SYNC_PLAN_KIND => Some(OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND),
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => { OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND) Some(OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND)
@@ -75,6 +81,7 @@ pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'stati
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("openai:chat"), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("openai:chat"),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => Some("gemini:interactions"),
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"), OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"),
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"), OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"),
OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND => Some("openai:embedding"), OPENAI_EMBEDDING_SYNC_FINALIZE_REPORT_KIND => Some("openai:embedding"),
@@ -92,6 +99,9 @@ pub fn core_error_background_report_kind(report_kind: &str) -> Option<&'static s
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_ERROR_REPORT_KIND), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_ERROR_REPORT_KIND),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_ERROR_REPORT_KIND),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_ERROR_REPORT_KIND), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_ERROR_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => {
Some(GEMINI_INTERACTIONS_SYNC_ERROR_REPORT_KIND)
}
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND), OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_RESPONSES_SYNC_ERROR_REPORT_KIND),
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => { OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND) Some(OPENAI_RESPONSES_COMPACT_SYNC_ERROR_REPORT_KIND)
@@ -115,6 +125,9 @@ pub fn core_success_background_report_kind(report_kind: &str) -> Option<&'static
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND), OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_CHAT_SYNC_SUCCESS_REPORT_KIND),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND), CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(CLAUDE_CHAT_SYNC_SUCCESS_REPORT_KIND),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND), GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some(GEMINI_CHAT_SYNC_SUCCESS_REPORT_KIND),
GEMINI_INTERACTIONS_SYNC_FINALIZE_REPORT_KIND => {
Some(GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND)
}
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND), OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some(OPENAI_IMAGE_SYNC_SUCCESS_REPORT_KIND),
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => { OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => {
Some(OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND) Some(OPENAI_RESPONSES_SYNC_SUCCESS_REPORT_KIND)
+54 -2
View File
@@ -27,6 +27,7 @@ pub enum FormatId {
OpenAiRerank, OpenAiRerank,
ClaudeMessages, ClaudeMessages,
GeminiGenerateContent, GeminiGenerateContent,
GeminiInteractions,
GeminiEmbedding, GeminiEmbedding,
JinaEmbedding, JinaEmbedding,
JinaRerank, JinaRerank,
@@ -51,7 +52,9 @@ impl FormatId {
| Self::OpenAiEmbedding | Self::OpenAiEmbedding
| Self::OpenAiRerank => FormatFamily::OpenAi, | Self::OpenAiRerank => FormatFamily::OpenAi,
Self::ClaudeMessages => FormatFamily::Claude, Self::ClaudeMessages => FormatFamily::Claude,
Self::GeminiGenerateContent | Self::GeminiEmbedding => FormatFamily::Gemini, Self::GeminiGenerateContent | Self::GeminiInteractions | Self::GeminiEmbedding => {
FormatFamily::Gemini
}
Self::JinaEmbedding | Self::JinaRerank => FormatFamily::Jina, Self::JinaEmbedding | Self::JinaRerank => FormatFamily::Jina,
Self::DoubaoEmbedding => FormatFamily::Doubao, Self::DoubaoEmbedding => FormatFamily::Doubao,
Self::AliyunMultimodalEmbedding => FormatFamily::Aliyun, Self::AliyunMultimodalEmbedding => FormatFamily::Aliyun,
@@ -74,6 +77,7 @@ impl FormatId {
Self::OpenAiRerank => "openai:rerank", Self::OpenAiRerank => "openai:rerank",
Self::ClaudeMessages => "claude:messages", Self::ClaudeMessages => "claude:messages",
Self::GeminiGenerateContent => "gemini:generate_content", Self::GeminiGenerateContent => "gemini:generate_content",
Self::GeminiInteractions => "gemini:interactions",
Self::GeminiEmbedding => "gemini:embedding", Self::GeminiEmbedding => "gemini:embedding",
Self::JinaEmbedding => "jina:embedding", Self::JinaEmbedding => "jina:embedding",
Self::JinaRerank => "jina:rerank", Self::JinaRerank => "jina:rerank",
@@ -103,6 +107,12 @@ impl FromStr for FormatId {
"openai:rerank" | "/v1/rerank" => Ok(Self::OpenAiRerank), "openai:rerank" | "/v1/rerank" => Ok(Self::OpenAiRerank),
"claude:messages" | "/v1/messages" => Ok(Self::ClaudeMessages), "claude:messages" | "/v1/messages" => Ok(Self::ClaudeMessages),
"gemini:generate_content" => Ok(Self::GeminiGenerateContent), "gemini:generate_content" => Ok(Self::GeminiGenerateContent),
"gemini:interactions"
| "gemini:interaction"
| "gemini_interactions"
| "gemini_interaction"
| "/v1/interactions"
| "/v1beta/interactions" => Ok(Self::GeminiInteractions),
"gemini:embedding" => Ok(Self::GeminiEmbedding), "gemini:embedding" => Ok(Self::GeminiEmbedding),
"jina:embedding" | "/jina/v1/embeddings" => Ok(Self::JinaEmbedding), "jina:embedding" | "/jina/v1/embeddings" => Ok(Self::JinaEmbedding),
"jina:rerank" | "/jina/v1/rerank" => Ok(Self::JinaRerank), "jina:rerank" | "/jina/v1/rerank" => Ok(Self::JinaRerank),
@@ -157,7 +167,12 @@ pub fn is_openai_responses_family_format(value: &str) -> bool {
pub fn api_format_uses_body_stream_field(value: &str) -> bool { pub fn api_format_uses_body_stream_field(value: &str) -> bool {
matches!( matches!(
FormatId::parse(value).map(FormatId::canonical), FormatId::parse(value).map(FormatId::canonical),
Some(FormatId::OpenAiChat | FormatId::OpenAiResponses | FormatId::ClaudeMessages) Some(
FormatId::OpenAiChat
| FormatId::OpenAiResponses
| FormatId::ClaudeMessages
| FormatId::GeminiInteractions,
)
) )
} }
@@ -237,6 +252,33 @@ mod tests {
} }
} }
#[test]
fn parses_gemini_interactions_api_formats() {
use super::{FormatFamily, FormatProfile};
assert_eq!(
FormatId::parse("gemini:interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::parse("gemini_interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::parse("/v1/interactions"),
Some(FormatId::GeminiInteractions)
);
assert_eq!(
FormatId::GeminiInteractions.to_string(),
"gemini:interactions"
);
assert_eq!(FormatId::GeminiInteractions.family(), FormatFamily::Gemini);
assert_eq!(
FormatId::GeminiInteractions.profile(),
FormatProfile::Default
);
}
#[test] #[test]
fn parses_rerank_api_formats() { fn parses_rerank_api_formats() {
assert_eq!( assert_eq!(
@@ -293,6 +335,10 @@ mod tests {
normalize_api_format_alias("GEMINI:GENERATE_CONTENT"), normalize_api_format_alias("GEMINI:GENERATE_CONTENT"),
"gemini:generate_content" "gemini:generate_content"
); );
assert_eq!(
normalize_api_format_alias("GEMINI_INTERACTIONS"),
"gemini:interactions"
);
assert_eq!( assert_eq!(
normalize_api_format_alias("OPENAI:EMBEDDING"), normalize_api_format_alias("OPENAI:EMBEDDING"),
"openai:embedding" "openai:embedding"
@@ -331,6 +377,10 @@ mod tests {
api_format_storage_aliases("gemini:generate_content"), api_format_storage_aliases("gemini:generate_content"),
vec!["gemini:generate_content".to_string()] vec!["gemini:generate_content".to_string()]
); );
assert_eq!(
api_format_storage_aliases("gemini:interactions"),
vec!["gemini:interactions".to_string()]
);
assert_eq!( assert_eq!(
api_format_storage_aliases("openai:embedding"), api_format_storage_aliases("openai:embedding"),
vec!["openai:embedding".to_string()] vec!["openai:embedding".to_string()]
@@ -364,6 +414,8 @@ mod tests {
assert!(api_format_uses_body_stream_field("/v1/responses")); assert!(api_format_uses_body_stream_field("/v1/responses"));
assert!(api_format_uses_body_stream_field("claude:messages")); assert!(api_format_uses_body_stream_field("claude:messages"));
assert!(api_format_uses_body_stream_field("/v1/messages")); assert!(api_format_uses_body_stream_field("/v1/messages"));
assert!(api_format_uses_body_stream_field("gemini:interactions"));
assert!(api_format_uses_body_stream_field("/v1/interactions"));
assert!(!api_format_uses_body_stream_field( assert!(!api_format_uses_body_stream_field(
"openai:responses:compact" "openai:responses:compact"
)); ));
+50 -5
View File
@@ -45,6 +45,7 @@ const EMBEDDING_CANDIDATE_API_FORMATS: &[&str] = &[
"aliyun:multimodal_embedding", "aliyun:multimodal_embedding",
]; ];
const RERANK_CANDIDATE_API_FORMATS: &[&str] = &["openai:rerank", "jina:rerank"]; const RERANK_CANDIDATE_API_FORMATS: &[&str] = &["openai:rerank", "jina:rerank"];
const GEMINI_INTERACTIONS_CANDIDATE_API_FORMATS: &[&str] = &["gemini:interactions"];
pub fn request_candidate_api_format_preference( pub fn request_candidate_api_format_preference(
client_api_format: &str, client_api_format: &str,
@@ -56,6 +57,9 @@ pub fn request_candidate_api_format_preference(
if client_api_format == "openai:responses:compact" { if client_api_format == "openai:responses:compact" {
return (provider_api_format == "openai:responses:compact").then_some((0, 0)); return (provider_api_format == "openai:responses:compact").then_some((0, 0));
} }
if is_gemini_interactions_api_format(client_api_format.as_str()) {
return (provider_api_format == "gemini:interactions").then_some((0, 0));
}
if is_embedding_api_format(client_api_format.as_str()) { if is_embedding_api_format(client_api_format.as_str()) {
return is_embedding_api_format(provider_api_format.as_str()).then_some(( return is_embedding_api_format(provider_api_format.as_str()).then_some((
if client_api_format == provider_api_format { if client_api_format == provider_api_format {
@@ -105,6 +109,9 @@ pub fn request_candidate_api_formats(
if client_api_format == "openai:responses:compact" { if client_api_format == "openai:responses:compact" {
return vec!["openai:responses:compact"]; return vec!["openai:responses:compact"];
} }
if is_gemini_interactions_api_format(client_api_format.as_str()) {
return GEMINI_INTERACTIONS_CANDIDATE_API_FORMATS.to_vec();
}
if is_embedding_api_format(client_api_format.as_str()) { if is_embedding_api_format(client_api_format.as_str()) {
let mut candidate_api_formats = EMBEDDING_CANDIDATE_API_FORMATS.to_vec(); let mut candidate_api_formats = EMBEDDING_CANDIDATE_API_FORMATS.to_vec();
candidate_api_formats.sort_by_key(|provider_api_format| { candidate_api_formats.sort_by_key(|provider_api_format| {
@@ -254,6 +261,10 @@ pub fn is_rerank_api_format(api_format: &str) -> bool {
) )
} }
pub fn is_gemini_interactions_api_format(api_format: &str) -> bool {
normalize_api_format_alias(api_format) == "gemini:interactions"
}
pub fn parse_non_compact_standard_api_format( pub fn parse_non_compact_standard_api_format(
api_format: &str, api_format: &str,
) -> Option<(&'static str, &'static str)> { ) -> Option<(&'static str, &'static str)> {
@@ -270,6 +281,7 @@ pub fn api_data_format_id(api_format: &str) -> Option<&'static str> {
match normalize_api_format_alias(api_format).as_str() { match normalize_api_format_alias(api_format).as_str() {
"claude:messages" => Some("claude"), "claude:messages" => Some("claude"),
"gemini:generate_content" => Some("gemini"), "gemini:generate_content" => Some("gemini"),
"gemini:interactions" => Some("gemini_interactions"),
"openai:chat" => Some("openai_chat"), "openai:chat" => Some("openai_chat"),
"openai:responses" | "openai:responses:compact" => Some("openai_responses"), "openai:responses" | "openai:responses:compact" => Some("openai_responses"),
"openai:embedding" "openai:embedding"
@@ -313,11 +325,12 @@ fn rerank_api_format_priority(api_format: &str) -> u8 {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
api_data_format_id, is_embedding_api_format, is_rerank_api_format, api_data_format_id, is_embedding_api_format, is_gemini_interactions_api_format,
request_candidate_api_format_preference, request_candidate_api_formats, is_rerank_api_format, request_candidate_api_format_preference,
request_conversion_kind, request_conversion_requires_enable_flag, request_candidate_api_formats, request_conversion_kind,
sync_chat_response_conversion_kind, sync_cli_response_conversion_kind, request_conversion_requires_enable_flag, sync_chat_response_conversion_kind,
RequestConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind, sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind,
SyncCliResponseConversionKind,
}; };
fn expected_request_conversion_kind(provider_api_format: &str) -> RequestConversionKind { fn expected_request_conversion_kind(provider_api_format: &str) -> RequestConversionKind {
@@ -581,6 +594,38 @@ mod tests {
} }
} }
#[test]
fn gemini_interactions_stays_same_format_only() {
assert!(is_gemini_interactions_api_format("gemini:interactions"));
assert_eq!(
request_candidate_api_formats("gemini:interactions", false),
vec!["gemini:interactions"]
);
assert_eq!(
request_candidate_api_format_preference("gemini:interactions", "gemini:interactions"),
Some((0, 0))
);
assert_eq!(
request_candidate_api_format_preference(
"gemini:interactions",
"gemini:generate_content"
),
None
);
assert_eq!(
request_conversion_kind("gemini:interactions", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:generate_content", "gemini:interactions"),
None
);
assert_eq!(
api_data_format_id("gemini:interactions"),
Some("gemini_interactions")
);
}
#[test] #[test]
fn rerank_candidate_registry_excludes_chat_and_embedding_formats() { fn rerank_candidate_registry_excludes_chat_and_embedding_formats() {
assert_eq!( assert_eq!(
@@ -38,6 +38,7 @@ pub fn parse_request(
FormatId::GeminiEmbedding => gemini::embedding::request::from(body, ctx), FormatId::GeminiEmbedding => gemini::embedding::request::from(body, ctx),
FormatId::DoubaoEmbedding => doubao::embedding::request::from(body, ctx), FormatId::DoubaoEmbedding => doubao::embedding::request::from(body, ctx),
FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::from(body, ctx), FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::from(body, ctx),
FormatId::GeminiInteractions => None,
} }
.ok_or_else(|| FormatError::RequestParseFailed { .ok_or_else(|| FormatError::RequestParseFailed {
format: source.as_str().to_string(), format: source.as_str().to_string(),
@@ -71,6 +72,7 @@ fn emit_request_inner(
FormatId::GeminiEmbedding => gemini::embedding::request::to(request, ctx), FormatId::GeminiEmbedding => gemini::embedding::request::to(request, ctx),
FormatId::DoubaoEmbedding => doubao::embedding::request::to(request, ctx), FormatId::DoubaoEmbedding => doubao::embedding::request::to(request, ctx),
FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::to(request, ctx), FormatId::AliyunMultimodalEmbedding => aliyun::embedding::request::to(request, ctx),
FormatId::GeminiInteractions => None,
} }
.ok_or_else(|| FormatError::RequestEmitFailed { .ok_or_else(|| FormatError::RequestEmitFailed {
format: target.as_str().to_string(), format: target.as_str().to_string(),
@@ -194,7 +196,8 @@ pub fn parse_response(
| FormatId::JinaRerank | FormatId::JinaRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
| FormatId::DoubaoEmbedding | FormatId::DoubaoEmbedding
| FormatId::AliyunMultimodalEmbedding => None, | FormatId::AliyunMultimodalEmbedding
| FormatId::GeminiInteractions => None,
} }
.ok_or_else(|| FormatError::ResponseParseFailed { .ok_or_else(|| FormatError::ResponseParseFailed {
format: source.as_str().to_string(), format: source.as_str().to_string(),
@@ -231,7 +234,8 @@ fn emit_response_inner(
| FormatId::JinaRerank | FormatId::JinaRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
| FormatId::DoubaoEmbedding | FormatId::DoubaoEmbedding
| FormatId::AliyunMultimodalEmbedding => None, | FormatId::AliyunMultimodalEmbedding
| FormatId::GeminiInteractions => None,
} }
.ok_or_else(|| FormatError::ResponseEmitFailed { .ok_or_else(|| FormatError::ResponseEmitFailed {
format: target.as_str().to_string(), format: target.as_str().to_string(),
@@ -523,6 +527,24 @@ fn standard_request_root_field_is_audited(source: FormatId, key: &str) -> bool {
| "tool_config" | "tool_config"
| "tools" | "tools"
), ),
FormatId::GeminiInteractions => matches!(
key,
"agent"
| "agent_config"
| "background"
| "generation_config"
| "input"
| "metadata"
| "model"
| "previous_interaction_id"
| "response_format"
| "safety_settings"
| "store"
| "stream"
| "system_instruction"
| "tool_config"
| "tools"
),
FormatId::OpenAiEmbedding FormatId::OpenAiEmbedding
| FormatId::OpenAiRerank | FormatId::OpenAiRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
@@ -981,6 +1003,7 @@ fn validate_source_response_stop_enums(
} }
FormatId::ClaudeMessages => validate_claude_response_stop_reason(body), FormatId::ClaudeMessages => validate_claude_response_stop_reason(body),
FormatId::GeminiGenerateContent => validate_gemini_response_finish_reasons(body, target), FormatId::GeminiGenerateContent => validate_gemini_response_finish_reasons(body, target),
FormatId::GeminiInteractions => Ok(()),
FormatId::OpenAiEmbedding FormatId::OpenAiEmbedding
| FormatId::OpenAiRerank | FormatId::OpenAiRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
@@ -2,7 +2,9 @@ use crate::contracts::{
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
}; };
@@ -58,6 +60,13 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
family: LocalSameFormatProviderFamily::Gemini, family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false, require_streaming: false,
}), }),
GEMINI_INTERACTIONS_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:interactions",
decision_kind: GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
report_kind: GEMINI_INTERACTIONS_SYNC_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec { OPENAI_EMBEDDING_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "openai:embedding", api_format: "openai:embedding",
decision_kind: OPENAI_EMBEDDING_SYNC_PLAN_KIND, decision_kind: OPENAI_EMBEDDING_SYNC_PLAN_KIND,
@@ -106,6 +115,13 @@ pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpe
family: LocalSameFormatProviderFamily::Gemini, family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true, require_streaming: true,
}), }),
GEMINI_INTERACTIONS_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:interactions",
decision_kind: GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
report_kind: GEMINI_INTERACTIONS_STREAM_SUCCESS_REPORT_KIND,
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true,
}),
_ => None, _ => None,
} }
} }
@@ -147,6 +163,21 @@ mod tests {
assert!(!spec.require_streaming); assert!(!spec.require_streaming);
} }
#[test]
fn resolves_gemini_interactions_same_format_specs() {
let sync = resolve_sync_spec("gemini_interactions_sync").expect("sync spec");
assert_eq!(sync.api_format, "gemini:interactions");
assert_eq!(sync.report_kind, "gemini_interactions_sync_success");
assert_eq!(sync.family, super::LocalSameFormatProviderFamily::Gemini);
assert!(!sync.require_streaming);
let stream = resolve_stream_spec("gemini_interactions_stream").expect("stream spec");
assert_eq!(stream.api_format, "gemini:interactions");
assert_eq!(stream.report_kind, "gemini_interactions_stream_success");
assert_eq!(stream.family, super::LocalSameFormatProviderFamily::Gemini);
assert!(stream.require_streaming);
}
#[test] #[test]
fn resolves_openai_rerank_sync_same_format_spec() { fn resolves_openai_rerank_sync_same_format_spec() {
let spec = resolve_sync_spec("openai_rerank_sync").expect("spec"); let spec = resolve_sync_spec("openai_rerank_sync").expect("spec");
@@ -6,14 +6,16 @@ use crate::contracts::{
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND, GEMINI_FILES_DELETE_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND, GEMINI_FILES_GET_PLAN_KIND,
GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_FILES_LIST_PLAN_KIND, GEMINI_FILES_UPLOAD_PLAN_KIND,
GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, GEMINI_VIDEO_CREATE_SYNC_PLAN_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
OPENAI_RESPONSES_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_RESPONSES_SYNC_PLAN_KIND,
OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND, OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_VIDEO_CONTENT_PLAN_KIND,
OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
}; };
use crate::formats::openai::image::request::is_openai_image_stream_request; use crate::formats::openai::image::request::is_openai_image_stream_request;
@@ -69,6 +71,14 @@ pub fn resolve_execution_runtime_stream_plan_kind(
)); ));
} }
if route_family == Some("gemini")
&& route_kind == Some("interactions")
&& *method == Method::POST
&& matches!(path, "/v1/interactions" | "/v1beta/interactions")
{
return Some(GEMINI_INTERACTIONS_STREAM_PLAN_KIND);
}
if route_family == Some("antigravity") if route_family == Some("antigravity")
&& route_kind == Some("stream_generate_content") && route_kind == Some("stream_generate_content")
&& *method == Method::POST && *method == Method::POST
@@ -182,6 +192,14 @@ pub fn resolve_execution_runtime_sync_plan_kind(
return Some(GEMINI_EMBEDDING_SYNC_PLAN_KIND); return Some(GEMINI_EMBEDDING_SYNC_PLAN_KIND);
} }
if route_family == Some("gemini")
&& route_kind == Some("interactions")
&& *method == Method::POST
&& matches!(path, "/v1/interactions" | "/v1beta/interactions")
{
return Some(GEMINI_INTERACTIONS_SYNC_PLAN_KIND);
}
if route_family == Some("openai") if route_family == Some("openai")
&& route_kind == Some("chat") && route_kind == Some("chat")
&& *method == Method::POST && *method == Method::POST
@@ -390,7 +408,8 @@ pub fn is_matching_stream_request(
| OPENAI_RESPONSES_STREAM_PLAN_KIND | OPENAI_RESPONSES_STREAM_PLAN_KIND
| OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND | OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND
| CLAUDE_CLI_STREAM_PLAN_KIND | CLAUDE_CLI_STREAM_PLAN_KIND
| OPENAI_IMAGE_STREAM_PLAN_KIND => body_json | OPENAI_IMAGE_STREAM_PLAN_KIND
| GEMINI_INTERACTIONS_STREAM_PLAN_KIND => body_json
.get("stream") .get("stream")
.and_then(|value| value.as_bool()) .and_then(|value| value.as_bool())
.unwrap_or(false), .unwrap_or(false),
@@ -428,6 +447,7 @@ pub fn supports_sync_execution_decision_kind(plan_kind: &str) -> bool {
| GEMINI_CHAT_SYNC_PLAN_KIND | GEMINI_CHAT_SYNC_PLAN_KIND
| GEMINI_CLI_SYNC_PLAN_KIND | GEMINI_CLI_SYNC_PLAN_KIND
| GEMINI_EMBEDDING_SYNC_PLAN_KIND | GEMINI_EMBEDDING_SYNC_PLAN_KIND
| GEMINI_INTERACTIONS_SYNC_PLAN_KIND
| GEMINI_FILES_UPLOAD_PLAN_KIND | GEMINI_FILES_UPLOAD_PLAN_KIND
| OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND | OPENAI_VIDEO_CREATE_SYNC_PLAN_KIND
| OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND | OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND
@@ -452,6 +472,7 @@ pub fn supports_stream_execution_decision_kind(plan_kind: &str) -> bool {
| OPENAI_IMAGE_STREAM_PLAN_KIND | OPENAI_IMAGE_STREAM_PLAN_KIND
| CLAUDE_CLI_STREAM_PLAN_KIND | CLAUDE_CLI_STREAM_PLAN_KIND
| GEMINI_CLI_STREAM_PLAN_KIND | GEMINI_CLI_STREAM_PLAN_KIND
| GEMINI_INTERACTIONS_STREAM_PLAN_KIND
| GEMINI_FILES_DOWNLOAD_PLAN_KIND | GEMINI_FILES_DOWNLOAD_PLAN_KIND
| OPENAI_VIDEO_CONTENT_PLAN_KIND | OPENAI_VIDEO_CONTENT_PLAN_KIND
) )
@@ -473,6 +494,7 @@ mod tests {
CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND, CLAUDE_CHAT_STREAM_PLAN_KIND, CLAUDE_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_STREAM_PLAN_KIND,
CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND, CLAUDE_CLI_SYNC_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CHAT_SYNC_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_CLI_SYNC_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND,
GEMINI_INTERACTIONS_STREAM_PLAN_KIND, GEMINI_INTERACTIONS_SYNC_PLAN_KIND,
OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_CHAT_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_COMPACT_SYNC_PLAN_KIND,
@@ -855,6 +877,62 @@ mod tests {
)); ));
} }
#[test]
fn resolves_gemini_interactions_plan_kinds() {
assert_eq!(
resolve_execution_runtime_sync_plan_kind(
Some("ai_public"),
Some("gemini"),
Some("interactions"),
Some("api_key"),
&Method::POST,
"/v1/interactions",
),
Some(GEMINI_INTERACTIONS_SYNC_PLAN_KIND)
);
assert_eq!(
resolve_execution_runtime_stream_plan_kind(
Some("ai_public"),
Some("gemini"),
Some("interactions"),
Some("api_key"),
&Method::POST,
"/v1/interactions",
),
Some(GEMINI_INTERACTIONS_STREAM_PLAN_KIND)
);
assert_eq!(
resolve_execution_runtime_sync_plan_kind(
Some("ai_public"),
Some("gemini"),
Some("interactions"),
Some("api_key"),
&Method::POST,
"/v1beta/interactions",
),
Some(GEMINI_INTERACTIONS_SYNC_PLAN_KIND)
);
}
#[test]
fn gemini_interactions_stream_plan_requires_body_stream_true() {
assert!(is_matching_stream_request(
GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
"/v1/interactions",
&serde_json::json!({"stream": true})
));
assert!(!is_matching_stream_request(
GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
"/v1/interactions",
&serde_json::json!({"stream": false})
));
assert!(!is_matching_stream_request(
GEMINI_INTERACTIONS_STREAM_PLAN_KIND,
"/v1/interactions",
&serde_json::json!({})
));
}
#[test] #[test]
fn resolves_openai_rerank_sync_plan_kind() { fn resolves_openai_rerank_sync_plan_kind() {
assert_eq!( assert_eq!(
@@ -274,6 +274,7 @@ impl ProviderStreamParser {
FormatId::OpenAiEmbedding FormatId::OpenAiEmbedding
| FormatId::OpenAiRerank | FormatId::OpenAiRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
| FormatId::GeminiInteractions
| FormatId::JinaEmbedding | FormatId::JinaEmbedding
| FormatId::JinaRerank | FormatId::JinaRerank
| FormatId::DoubaoEmbedding | FormatId::DoubaoEmbedding
@@ -365,6 +366,7 @@ impl ClientStreamEmitter {
FormatId::OpenAiEmbedding FormatId::OpenAiEmbedding
| FormatId::OpenAiRerank | FormatId::OpenAiRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
| FormatId::GeminiInteractions
| FormatId::JinaEmbedding | FormatId::JinaEmbedding
| FormatId::JinaRerank | FormatId::JinaRerank
| FormatId::DoubaoEmbedding | FormatId::DoubaoEmbedding
@@ -472,7 +474,9 @@ fn parse_provider_error(
parse_openai_error(payload) parse_openai_error(payload)
} }
FormatId::ClaudeMessages => parse_claude_error(payload), FormatId::ClaudeMessages => parse_claude_error(payload),
FormatId::GeminiGenerateContent => parse_gemini_error(payload), FormatId::GeminiGenerateContent | FormatId::GeminiInteractions => {
parse_gemini_error(payload)
}
FormatId::OpenAiEmbedding FormatId::OpenAiEmbedding
| FormatId::OpenAiRerank | FormatId::OpenAiRerank
| FormatId::GeminiEmbedding | FormatId::GeminiEmbedding
+5 -5
View File
@@ -16,11 +16,11 @@ pub use formats::id::{
is_openai_responses_format, normalize_api_format_alias, FormatFamily, FormatId, FormatProfile, is_openai_responses_format, normalize_api_format_alias, FormatFamily, FormatId, FormatProfile,
}; };
pub use formats::matrix::{ pub use formats::matrix::{
is_embedding_api_format, is_rerank_api_format, request_candidate_api_format_preference, is_embedding_api_format, is_gemini_interactions_api_format, is_rerank_api_format,
request_candidate_api_formats, request_conversion_kind, request_candidate_api_format_preference, request_candidate_api_formats,
request_conversion_requires_enable_flag, sync_chat_response_conversion_kind, request_conversion_kind, request_conversion_requires_enable_flag,
sync_cli_response_conversion_kind, RequestConversionKind, SyncChatResponseConversionKind, sync_chat_response_conversion_kind, sync_cli_response_conversion_kind, RequestConversionKind,
SyncCliResponseConversionKind, SyncChatResponseConversionKind, SyncCliResponseConversionKind,
}; };
pub use formats::registry::{ pub use formats::registry::{
build_stream_transcoder, convert_request, convert_request_pure, build_stream_transcoder, convert_request, convert_request_pure,
+26 -1
View File
@@ -142,6 +142,17 @@ pub fn extract_ai_standard_requested_model(body_json: &Value) -> Option<String>
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
} }
pub fn extract_ai_gemini_body_requested_target(body_json: &Value) -> Option<String> {
extract_ai_standard_requested_model(body_json).or_else(|| {
body_json
.get("agent")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
}
pub fn extract_ai_requested_model_from_request_path( pub fn extract_ai_requested_model_from_request_path(
request_path: &str, request_path: &str,
body_json: &Value, body_json: &Value,
@@ -150,7 +161,7 @@ pub fn extract_ai_requested_model_from_request_path(
match family { match family {
AiRequestedModelFamily::Standard => extract_ai_standard_requested_model(body_json), AiRequestedModelFamily::Standard => extract_ai_standard_requested_model(body_json),
AiRequestedModelFamily::Gemini => extract_ai_gemini_model_from_path(request_path) AiRequestedModelFamily::Gemini => extract_ai_gemini_model_from_path(request_path)
.or_else(|| extract_ai_standard_requested_model(body_json)), .or_else(|| extract_ai_gemini_body_requested_target(body_json)),
} }
} }
@@ -245,4 +256,18 @@ mod tests {
assert_eq!(requested_model.as_deref(), Some("gemini-cli")); assert_eq!(requested_model.as_deref(), Some("gemini-cli"));
} }
#[test]
fn gemini_requested_model_parser_uses_body_agent_when_path_has_no_model() {
let requested_model = extract_ai_requested_model_from_request_path(
"/v1/interactions",
&serde_json::json!({ "agent": " antigravity-preview-05-2026 " }),
AiRequestedModelFamily::Gemini,
);
assert_eq!(
requested_model.as_deref(),
Some("antigravity-preview-05-2026")
);
}
} }
@@ -0,0 +1,47 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'providers'
AND column_name = 'enabled'
) THEN
UPDATE public.providers
SET
is_active = enabled,
updated_at = NOW()
WHERE enabled IS NOT NULL
AND is_active IS DISTINCT FROM enabled;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'provider_endpoints'
AND column_name = 'enabled'
) THEN
UPDATE public.provider_endpoints
SET
is_active = enabled,
updated_at = NOW()
WHERE enabled IS NOT NULL
AND is_active IS DISTINCT FROM enabled;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'models'
AND column_name = 'enabled'
) THEN
UPDATE public.models
SET
is_active = enabled,
updated_at = NOW()
WHERE enabled IS NOT NULL
AND is_active IS DISTINCT FROM enabled;
END IF;
END $$;
+13 -5
View File
@@ -312,7 +312,8 @@ mod tests {
20260422110000, 20260422110000,
20260422120000, 20260422120000,
20260504120000, 20260504120000,
20260505120000 20260505120000,
20260517012000
] ]
); );
} }
@@ -328,7 +329,12 @@ mod tests {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
assert_eq!( assert_eq!(
versions, versions,
vec![20260422120000, 20260504120000, 20260505120000] vec![
20260422120000,
20260504120000,
20260505120000,
20260517012000
]
); );
} }
@@ -697,11 +703,12 @@ mod tests {
let pending_before = pending_backfills(&pool) let pending_before = pending_backfills(&pool)
.await .await
.expect("pending backfills should load"); .expect("pending backfills should load");
assert_eq!(pending_before.len(), 4); assert_eq!(pending_before.len(), 5);
assert_eq!(pending_before[0].version, 20260422110000); assert_eq!(pending_before[0].version, 20260422110000);
assert_eq!(pending_before[1].version, 20260422120000); assert_eq!(pending_before[1].version, 20260422120000);
assert_eq!(pending_before[2].version, 20260504120000); assert_eq!(pending_before[2].version, 20260504120000);
assert_eq!(pending_before[3].version, 20260505120000); assert_eq!(pending_before[3].version, 20260505120000);
assert_eq!(pending_before[4].version, 20260517012000);
run_backfills(&pool) run_backfills(&pool)
.await .await
@@ -723,7 +730,8 @@ mod tests {
20260422110000, 20260422110000,
20260422120000, 20260422120000,
20260504120000, 20260504120000,
20260505120000 20260505120000,
20260517012000
] ]
); );
@@ -1385,6 +1393,6 @@ mod tests {
.fetch_one(&pool) .fetch_one(&pool)
.await .await
.expect("backfill count should load"); .expect("backfill count should load");
assert_eq!(applied_count, 3); assert_eq!(applied_count, 5);
} }
} }
+3 -2
View File
@@ -727,6 +727,7 @@ mod tests {
build_gemini_cli_load_code_assist_plan, build_kiro_list_available_models_plan, build_gemini_cli_load_code_assist_plan, build_kiro_list_available_models_plan,
build_models_fetch_execution_plan, build_standard_models_fetch_execution_plan, build_models_fetch_execution_plan, build_standard_models_fetch_execution_plan,
build_vertex_models_fetch_execution_plan, ModelFetchTransportRuntime, build_vertex_models_fetch_execution_plan, ModelFetchTransportRuntime,
ANTIGRAVITY_REQUEST_USER_AGENT,
}; };
struct TestRuntime { struct TestRuntime {
@@ -1053,7 +1054,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
plan.headers.get("user-agent").map(String::as_str), plan.headers.get("user-agent").map(String::as_str),
Some("antigravity") Some(ANTIGRAVITY_REQUEST_USER_AGENT)
); );
assert_eq!( assert_eq!(
plan.headers.get("x-client-name").map(String::as_str), plan.headers.get("x-client-name").map(String::as_str),
@@ -1100,7 +1101,7 @@ mod tests {
); );
assert_eq!( assert_eq!(
plan.headers.get("user-agent").map(String::as_str), plan.headers.get("user-agent").map(String::as_str),
Some("antigravity") Some(ANTIGRAVITY_REQUEST_USER_AGENT)
); );
assert_eq!( assert_eq!(
plan.headers.get("x-client-name").map(String::as_str), plan.headers.get("x-client-name").map(String::as_str),
@@ -5,7 +5,8 @@ use serde_json::Value;
use super::super::snapshot::GatewayProviderTransportSnapshot; use super::super::snapshot::GatewayProviderTransportSnapshot;
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity"; pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
pub const ANTIGRAVITY_REQUEST_USER_AGENT: &str = "antigravity"; pub const ANTIGRAVITY_REQUEST_USER_AGENT: &str =
"antigravity/cli/1.0.16 (aidev_client; os_type=linux; arch=arm64; auth_method=consumer)";
const ANTIGRAVITY_CLIENT_NAME: &str = "antigravity"; const ANTIGRAVITY_CLIENT_NAME: &str = "antigravity";
const ANTIGRAVITY_GOOG_API_CLIENT: &str = "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x"; const ANTIGRAVITY_GOOG_API_CLIENT: &str = "gl-node/18.18.2 fire/0.8.6 grpc/1.10.x";
@@ -120,6 +121,10 @@ pub fn build_antigravity_static_client_headers(
String::from("x-goog-api-client"), String::from("x-goog-api-client"),
String::from(ANTIGRAVITY_GOOG_API_CLIENT), String::from(ANTIGRAVITY_GOOG_API_CLIENT),
), ),
(
String::from("user-agent"),
String::from(ANTIGRAVITY_REQUEST_USER_AGENT),
),
]); ]);
if let Some(client_version) = client_version if let Some(client_version) = client_version
@@ -267,8 +272,8 @@ mod tests {
use serde_json::json; use serde_json::json;
use super::{ use super::{
resolve_local_antigravity_request_auth, AntigravityRequestAuth, build_antigravity_static_client_headers, resolve_local_antigravity_request_auth,
AntigravityRequestAuthSupport, AntigravityRequestAuth, AntigravityRequestAuthSupport, ANTIGRAVITY_REQUEST_USER_AGENT,
}; };
use crate::snapshot::{ use crate::snapshot::{
GatewayProviderTransportEndpoint, GatewayProviderTransportKey, GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
@@ -376,4 +381,26 @@ mod tests {
}) })
); );
} }
#[test]
fn static_client_headers_use_native_antigravity_cli_user_agent() {
let headers = build_antigravity_static_client_headers(Some("1.0.16"), Some("session-abc"));
assert_eq!(
headers.get("user-agent").map(String::as_str),
Some(ANTIGRAVITY_REQUEST_USER_AGENT)
);
assert_eq!(
headers.get("x-client-name").map(String::as_str),
Some("antigravity")
);
assert_eq!(
headers.get("x-client-version").map(String::as_str),
Some("1.0.16")
);
assert_eq!(
headers.get("x-vscode-sessionid").map(String::as_str),
Some("session-abc")
);
}
} }
@@ -141,6 +141,7 @@ mod tests {
build_antigravity_safe_v1internal_request, classify_antigravity_safe_request_body, build_antigravity_safe_v1internal_request, classify_antigravity_safe_request_body,
AntigravityEnvelopeRequestType, AntigravityRequestAuth, AntigravityRequestEnvelopeSupport, AntigravityEnvelopeRequestType, AntigravityRequestAuth, AntigravityRequestEnvelopeSupport,
}; };
use crate::antigravity::ANTIGRAVITY_REQUEST_USER_AGENT;
fn sample_auth() -> AntigravityRequestAuth { fn sample_auth() -> AntigravityRequestAuth {
AntigravityRequestAuth { AntigravityRequestAuth {
@@ -228,7 +229,7 @@ mod tests {
assert_eq!(envelope["project"], "project-ant-123"); assert_eq!(envelope["project"], "project-ant-123");
assert_eq!(envelope["requestId"], "request-ant-agent-123"); assert_eq!(envelope["requestId"], "request-ant-agent-123");
assert_eq!(envelope["model"], "gemini-3.5-flash-low"); assert_eq!(envelope["model"], "gemini-3.5-flash-low");
assert_eq!(envelope["userAgent"], "antigravity"); assert_eq!(envelope["userAgent"], ANTIGRAVITY_REQUEST_USER_AGENT);
assert_eq!(envelope["requestType"], "agent"); assert_eq!(envelope["requestType"], "agent");
assert!(envelope["request"].get("model").is_none()); assert!(envelope["request"].get("model").is_none());
assert!(envelope["request"].get("safetySettings").is_none()); assert!(envelope["request"].get("safetySettings").is_none());
@@ -4,6 +4,7 @@ use aether_ai_formats::formats::matrix::{
}; };
use aether_ai_formats::normalize_api_format_alias; use aether_ai_formats::normalize_api_format_alias;
use crate::antigravity::is_antigravity_provider_transport;
use crate::auth::{ use crate::auth::{
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth, resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
@@ -132,6 +133,11 @@ pub fn request_conversion_transport_unsupported_reason(
{ {
return local_windsurf_request_transport_unsupported_reason_with_network(transport); return local_windsurf_request_transport_unsupported_reason_with_network(transport);
} }
if is_antigravity_provider_transport(transport)
&& normalize_api_format_alias(&transport.endpoint.api_format) == "gemini:generate_content"
{
return None;
}
match normalize_api_format_alias(&transport.endpoint.api_format).as_str() { match normalize_api_format_alias(&transport.endpoint.api_format).as_str() {
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport), "openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
@@ -658,6 +664,32 @@ mod tests {
); );
} }
#[test]
fn antigravity_gemini_transport_supports_standard_cross_format_conversion_via_envelope() {
let transport = transport_snapshot(
"antigravity",
"gemini:generate_content",
"oauth",
true,
None,
);
assert!(request_pair_allowed_for_transport(
&transport,
"openai:chat",
"gemini:generate_content"
));
assert!(request_pair_allowed_for_transport(
&transport,
"openai:responses",
"gemini:generate_content"
));
assert!(request_conversion_transport_supported(
&transport,
RequestConversionKind::ToGeminiStandard
));
}
#[test] #[test]
fn vertex_gemini_embedding_transport_supports_openai_embedding_conversion() { fn vertex_gemini_embedding_transport_supports_openai_embedding_conversion() {
let transport = transport_snapshot("vertex_ai", "gemini:embedding", "api_key", true, None); let transport = transport_snapshot("vertex_ai", "gemini:embedding", "api_key", true, None);
@@ -150,6 +150,9 @@ fn build_transport_request_url_inner(
params.request_query, params.request_query,
gemini_embedding_batch, gemini_embedding_batch,
), ),
"gemini:interactions" => {
build_gemini_interactions_url(&transport.endpoint.base_url, params.request_query)
}
"doubao:embedding" => build_passthrough_path_url( "doubao:embedding" => build_passthrough_path_url(
&transport.endpoint.base_url, &transport.endpoint.base_url,
"/embeddings", "/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| { let query = params.request_query.map(|raw| {
form_urlencoded::parse(raw.as_bytes()) form_urlencoded::parse(raw.as_bytes())
.into_owned() .into_owned()
@@ -398,7 +403,8 @@ fn build_path_params(
} }
let provider_api_format = let provider_api_format =
aether_ai_formats::normalize_api_format_alias(params.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( path_params.insert(
"action", "action",
if provider_api_format == "gemini:embedding" { 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) 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( fn build_provider_api_root_url(
upstream_base_url: &str, upstream_base_url: &str,
path: &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] #[test]
fn embedding_request_url_preserves_google_openai_compat_roots() { fn embedding_request_url_preserves_google_openai_compat_roots() {
let developer_api_openai = sample_transport( let developer_api_openai = sample_transport(
@@ -265,13 +265,39 @@ fn build_same_format_provider_request_body_inner(
} }
} }
SameFormatProviderFamily::Gemini => { SameFormatProviderFamily::Gemini => {
if provider_request_body.remove("model").is_some() { if aether_ai_formats::api_format_alias_matches(
record_compatibility_edit( input.provider_api_format,
&mut compatibility_edits, "gemini:interactions",
"model", ) {
SameFormatProviderCompatibilityEditAction::RuntimeRewrite, let rewrite_field = if provider_request_body.contains_key("model") {
"removed top-level model because Gemini model is carried by the upstream URL", "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", "openai:responses:compact" => "openai:responses:compact",
"claude:messages" => "claude:messages", "claude:messages" => "claude:messages",
"gemini:generate_content" => "gemini:generate_content", "gemini:generate_content" => "gemini:generate_content",
"gemini:interactions" => "gemini:interactions",
_ => return Some("transport_api_format_unsupported"), _ => return Some("transport_api_format_unsupported"),
}; };
let behavior = classify_same_format_provider_request_behavior( let behavior = classify_same_format_provider_request_behavior(
@@ -1210,6 +1237,62 @@ mod tests {
assert!(body.get("stream").is_none()); 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] #[test]
fn same_format_gemini_body_strips_function_response_id_for_upstream() { fn same_format_gemini_body_strips_function_response_id_for_upstream() {
let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput { let body = build_same_format_provider_request_body(SameFormatProviderRequestBodyInput {
@@ -17,6 +17,7 @@ describe('api format display helpers', () => {
expect(normalizeApiFormatAlias('GEMINI_GENERATE_CONTENT')).toBe(API_FORMATS.GEMINI_GENERATE_CONTENT) expect(normalizeApiFormatAlias('GEMINI_GENERATE_CONTENT')).toBe(API_FORMATS.GEMINI_GENERATE_CONTENT)
expect(normalizeApiFormatAlias('OPENAI_EMBEDDING')).toBe(API_FORMATS.OPENAI_EMBEDDING) expect(normalizeApiFormatAlias('OPENAI_EMBEDDING')).toBe(API_FORMATS.OPENAI_EMBEDDING)
expect(normalizeApiFormatAlias('OPENAI_RERANK')).toBe(API_FORMATS.OPENAI_RERANK) expect(normalizeApiFormatAlias('OPENAI_RERANK')).toBe(API_FORMATS.OPENAI_RERANK)
expect(normalizeApiFormatAlias('GEMINI_INTERACTIONS')).toBe(API_FORMATS.GEMINI_INTERACTIONS)
expect(normalizeApiFormatAlias('GEMINI_EMBEDDING')).toBe(API_FORMATS.GEMINI_EMBEDDING) expect(normalizeApiFormatAlias('GEMINI_EMBEDDING')).toBe(API_FORMATS.GEMINI_EMBEDDING)
expect(normalizeApiFormatAlias('JINA_EMBEDDING')).toBe(API_FORMATS.JINA_EMBEDDING) expect(normalizeApiFormatAlias('JINA_EMBEDDING')).toBe(API_FORMATS.JINA_EMBEDDING)
expect(normalizeApiFormatAlias('JINA_RERANK')).toBe(API_FORMATS.JINA_RERANK) expect(normalizeApiFormatAlias('JINA_RERANK')).toBe(API_FORMATS.JINA_RERANK)
@@ -33,6 +34,8 @@ describe('api format display helpers', () => {
}) })
it('formats embedding api format ids distinctly from chat formats', () => { it('formats embedding api format ids distinctly from chat formats', () => {
expect(formatApiFormat(API_FORMATS.GEMINI_INTERACTIONS)).toBe('Gemini Interactions')
expect(formatApiFormatShort(API_FORMATS.GEMINI_INTERACTIONS)).toBe('GI')
expect(formatApiFormat(API_FORMATS.OPENAI_EMBEDDING)).toBe('OpenAI Embedding') expect(formatApiFormat(API_FORMATS.OPENAI_EMBEDDING)).toBe('OpenAI Embedding')
expect(formatApiFormat(API_FORMATS.GEMINI_EMBEDDING)).toBe('Gemini Embedding') expect(formatApiFormat(API_FORMATS.GEMINI_EMBEDDING)).toBe('Gemini Embedding')
expect(formatApiFormat(API_FORMATS.JINA_EMBEDDING)).toBe('Jina Embedding') expect(formatApiFormat(API_FORMATS.JINA_EMBEDDING)).toBe('Jina Embedding')
@@ -73,6 +76,7 @@ describe('api format display helpers', () => {
API_FORMATS.OPENAI_RESPONSES, API_FORMATS.OPENAI_RESPONSES,
API_FORMATS.OPENAI_EMBEDDING, API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK, API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_INTERACTIONS,
API_FORMATS.GEMINI_EMBEDDING, API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.JINA_EMBEDDING, API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK, API_FORMATS.JINA_RERANK,
@@ -82,6 +86,7 @@ describe('api format display helpers', () => {
API_FORMATS.OPENAI_RESPONSES, API_FORMATS.OPENAI_RESPONSES,
API_FORMATS.OPENAI_EMBEDDING, API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK, API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_INTERACTIONS,
API_FORMATS.GEMINI_EMBEDDING, API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.JINA_EMBEDDING, API_FORMATS.JINA_EMBEDDING,
API_FORMATS.JINA_RERANK, API_FORMATS.JINA_RERANK,
@@ -97,12 +102,14 @@ describe('api format display helpers', () => {
API_FORMATS.OPENAI_EMBEDDING, API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK, API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_GENERATE_CONTENT, API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.GEMINI_INTERACTIONS,
API_FORMATS.OPENAI, API_FORMATS.OPENAI,
])).toEqual([ ])).toEqual([
API_FORMATS.OPENAI, API_FORMATS.OPENAI,
API_FORMATS.OPENAI_EMBEDDING, API_FORMATS.OPENAI_EMBEDDING,
API_FORMATS.OPENAI_RERANK, API_FORMATS.OPENAI_RERANK,
API_FORMATS.GEMINI_GENERATE_CONTENT, API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.GEMINI_INTERACTIONS,
API_FORMATS.GEMINI_EMBEDDING, API_FORMATS.GEMINI_EMBEDDING,
]) ])
}) })
@@ -12,6 +12,7 @@ export const API_FORMATS = {
OPENAI_RERANK: 'openai:rerank', OPENAI_RERANK: 'openai:rerank',
GEMINI: 'gemini:generate_content', GEMINI: 'gemini:generate_content',
GEMINI_GENERATE_CONTENT: 'gemini:generate_content', GEMINI_GENERATE_CONTENT: 'gemini:generate_content',
GEMINI_INTERACTIONS: 'gemini:interactions',
GEMINI_VIDEO: 'gemini:video', GEMINI_VIDEO: 'gemini:video',
GEMINI_FILES: 'gemini:files', GEMINI_FILES: 'gemini:files',
GEMINI_EMBEDDING: 'gemini:embedding', GEMINI_EMBEDDING: 'gemini:embedding',
@@ -34,6 +35,7 @@ export const API_FORMAT_LABELS: Record<string, string> = {
[API_FORMATS.OPENAI_EMBEDDING]: 'OpenAI Embedding', [API_FORMATS.OPENAI_EMBEDDING]: 'OpenAI Embedding',
[API_FORMATS.OPENAI_RERANK]: 'OpenAI Rerank', [API_FORMATS.OPENAI_RERANK]: 'OpenAI Rerank',
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'Gemini Generate Content', [API_FORMATS.GEMINI_GENERATE_CONTENT]: 'Gemini Generate Content',
[API_FORMATS.GEMINI_INTERACTIONS]: 'Gemini Interactions',
[API_FORMATS.GEMINI_VIDEO]: 'Gemini Video', [API_FORMATS.GEMINI_VIDEO]: 'Gemini Video',
[API_FORMATS.GEMINI_FILES]: 'Gemini Files', [API_FORMATS.GEMINI_FILES]: 'Gemini Files',
[API_FORMATS.GEMINI_EMBEDDING]: 'Gemini Embedding', [API_FORMATS.GEMINI_EMBEDDING]: 'Gemini Embedding',
@@ -52,6 +54,7 @@ export const API_FORMAT_LABELS: Record<string, string> = {
OPENAI_RERANK: 'OpenAI Rerank', OPENAI_RERANK: 'OpenAI Rerank',
GEMINI: 'Gemini Generate Content', GEMINI: 'Gemini Generate Content',
GEMINI_GENERATE_CONTENT: 'Gemini Generate Content', GEMINI_GENERATE_CONTENT: 'Gemini Generate Content',
GEMINI_INTERACTIONS: 'Gemini Interactions',
GEMINI_VIDEO: 'Gemini Video', GEMINI_VIDEO: 'Gemini Video',
GEMINI_FILES: 'Gemini Files', GEMINI_FILES: 'Gemini Files',
GEMINI_EMBEDDING: 'Gemini Embedding', GEMINI_EMBEDDING: 'Gemini Embedding',
@@ -72,6 +75,7 @@ export const API_FORMAT_SHORT: Record<string, string> = {
[API_FORMATS.OPENAI_RERANK]: 'ORR', [API_FORMATS.OPENAI_RERANK]: 'ORR',
[API_FORMATS.CLAUDE_MESSAGES]: 'CM', [API_FORMATS.CLAUDE_MESSAGES]: 'CM',
[API_FORMATS.GEMINI_GENERATE_CONTENT]: 'G', [API_FORMATS.GEMINI_GENERATE_CONTENT]: 'G',
[API_FORMATS.GEMINI_INTERACTIONS]: 'GI',
[API_FORMATS.GEMINI_VIDEO]: 'GV', [API_FORMATS.GEMINI_VIDEO]: 'GV',
[API_FORMATS.GEMINI_FILES]: 'GF', [API_FORMATS.GEMINI_FILES]: 'GF',
[API_FORMATS.GEMINI_EMBEDDING]: 'GE', [API_FORMATS.GEMINI_EMBEDDING]: 'GE',
@@ -90,6 +94,7 @@ export const API_FORMAT_SHORT: Record<string, string> = {
CLAUDE_MESSAGES: 'CM', CLAUDE_MESSAGES: 'CM',
GEMINI: 'G', GEMINI: 'G',
GEMINI_GENERATE_CONTENT: 'G', GEMINI_GENERATE_CONTENT: 'G',
GEMINI_INTERACTIONS: 'GI',
GEMINI_VIDEO: 'GV', GEMINI_VIDEO: 'GV',
GEMINI_FILES: 'GF', GEMINI_FILES: 'GF',
GEMINI_EMBEDDING: 'GE', GEMINI_EMBEDDING: 'GE',
@@ -110,6 +115,7 @@ export const API_FORMAT_ORDER: string[] = [
API_FORMATS.OPENAI_VIDEO, API_FORMATS.OPENAI_VIDEO,
API_FORMATS.CLAUDE_MESSAGES, API_FORMATS.CLAUDE_MESSAGES,
API_FORMATS.GEMINI_GENERATE_CONTENT, API_FORMATS.GEMINI_GENERATE_CONTENT,
API_FORMATS.GEMINI_INTERACTIONS,
API_FORMATS.GEMINI_EMBEDDING, API_FORMATS.GEMINI_EMBEDDING,
API_FORMATS.GEMINI_VIDEO, API_FORMATS.GEMINI_VIDEO,
API_FORMATS.GEMINI_FILES, API_FORMATS.GEMINI_FILES,
@@ -136,6 +142,7 @@ export const API_FORMAT_KIND_LABELS: Record<string, string> = {
'responses:compact': 'Responses Compact', 'responses:compact': 'Responses Compact',
messages: 'Messages', messages: 'Messages',
generate_content: 'Generate Content', generate_content: 'Generate Content',
interactions: 'Interactions',
image: 'Image', image: 'Image',
video: 'Video', video: 'Video',
files: 'Files', files: 'Files',
@@ -178,6 +185,8 @@ export function normalizeApiFormatAlias(format: string | null | undefined): stri
case 'GEMINI': case 'GEMINI':
case 'GEMINI_GENERATE_CONTENT': case 'GEMINI_GENERATE_CONTENT':
return API_FORMATS.GEMINI_GENERATE_CONTENT return API_FORMATS.GEMINI_GENERATE_CONTENT
case 'GEMINI_INTERACTIONS':
return API_FORMATS.GEMINI_INTERACTIONS
case 'GEMINI_VIDEO': case 'GEMINI_VIDEO':
return API_FORMATS.GEMINI_VIDEO return API_FORMATS.GEMINI_VIDEO
case 'GEMINI_FILES': case 'GEMINI_FILES':
+10 -3
View File
@@ -344,9 +344,16 @@ export interface CodexUpstreamMetadata {
} }
export interface AntigravityModelQuota { export interface AntigravityModelQuota {
remaining_fraction: number // 剩余比例 (0.0-1.0) remaining_fraction?: number | string | null // 剩余比例 (0.0-1.0)
used_percent: number // 已用百分比 (0.0-100.0) used_percent?: number | string | null // 已用百分比 (0.0-100.0)
reset_time?: string // RFC3339 remaining?: number | string | null
total?: number | string | null
reset_time?: string | null // RFC3339
reset_at?: number | string | null
display_name?: string | null
model_id?: string | null
token_type?: string | null
is_exhausted?: boolean | null
} }
export interface AntigravityUpstreamMetadata { export interface AntigravityUpstreamMetadata {
@@ -34,9 +34,10 @@
<DropdownMenuItem <DropdownMenuItem
v-for="item in items" v-for="item in items"
:key="item.model" :key="item.model"
:title="item.model"
@select="handleTestModel(item.model)" @select="handleTestModel(item.model)"
> >
{{ item.model }} <span class="truncate">{{ item.label }}</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -52,12 +53,14 @@
:key="item.model" :key="item.model"
> >
<div class="flex items-center justify-between text-[10px] mb-0.5"> <div class="flex items-center justify-between text-[10px] mb-0.5">
<span <div class="min-w-0 flex-1 mr-2">
class="text-muted-foreground truncate mr-2 min-w-0 flex-1" <div
:title="item.model" class="text-muted-foreground truncate"
> :title="item.model"
{{ item.label }} >
</span> {{ item.label }}
</div>
</div>
<span :class="getQuotaRemainingClass(item.usedPercent)"> <span :class="getQuotaRemainingClass(item.usedPercent)">
{{ item.remainingPercent.toFixed(1) }}% {{ item.remainingPercent.toFixed(1) }}%
</span> </span>
@@ -115,6 +118,11 @@ import { testModel } from '@/api/endpoints/providers'
import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types' import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser' import { parseApiError } from '@/utils/errorParser'
import {
compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@@ -165,6 +173,25 @@ function getQuotaWindowLiveResetSeconds(
return null return null
} }
function coercePercent(value: unknown): number | null {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return null
return Math.min(Math.max(numericValue, 0), 100)
}
function coerceRemainingFraction(value: unknown): number | null {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return null
return Math.min(Math.max(numericValue, 0), 1)
}
function secondsUntilUnixReset(resetAt: unknown): number | null {
const numericResetAt = Number(resetAt)
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
const now = Math.floor(Date.now() / 1000)
return Math.max(Math.floor(numericResetAt - now), 0)
}
function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] { function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] {
if (!quota) return [] if (!quota) return []
@@ -175,6 +202,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
? quota.windows.filter(window => String(window?.scope || '').trim().toLowerCase() === 'model') ? quota.windows.filter(window => String(window?.scope || '').trim().toLowerCase() === 'model')
: [] : []
if (windows.length === 0) return [] if (windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
const items = windows const items = windows
.map((window) => { .map((window) => {
@@ -196,7 +224,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
return { return {
model, model,
label: String(window.label || window.model || model), label: resolveAntigravityQuotaLabel(model, window.label || window.model, opaqueDisplayIndex),
usedPercent, usedPercent,
remainingPercent, remainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window), resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
@@ -204,8 +232,8 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
}) })
.filter((item): item is QuotaItem => item !== null) .filter((item): item is QuotaItem => item !== null)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model)) items.sort(compareAntigravityQuotaItems)
return items return dedupeAntigravityQuotaItemsByLabel(items)
} }
const items = computed<QuotaItem[]>(() => { const items = computed<QuotaItem[]>(() => {
@@ -218,26 +246,26 @@ const items = computed<QuotaItem[]>(() => {
if (!quotaByModel || typeof quotaByModel !== 'object') return [] if (!quotaByModel || typeof quotaByModel !== 'object') return []
const result: QuotaItem[] = [] const result: QuotaItem[] = []
const opaqueDisplayIndex = { value: 1 }
for (const [model, rawInfo] of Object.entries(quotaByModel)) { for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue if (!model) continue
const info = (rawInfo || {}) as Record<string, unknown> const info = (rawInfo || {}) as Record<string, unknown>
let usedPercent = Number(info['used_percent']) let usedPercent = coercePercent(info['used_percent'])
if (!Number.isFinite(usedPercent)) { if (usedPercent === null) {
const remainingFraction = Number(info['remaining_fraction']) const remainingFraction = coerceRemainingFraction(info['remaining_fraction'])
if (Number.isFinite(remainingFraction)) { if (remainingFraction !== null) {
usedPercent = (1 - remainingFraction) * 100 usedPercent = (1 - remainingFraction) * 100
} else { } else {
continue continue
} }
} }
if (usedPercent < 0) usedPercent = 0 usedPercent = coercePercent(usedPercent) ?? 0
if (usedPercent > 100) usedPercent = 100
const remainingPercent = Math.max(100 - usedPercent, 0) const remainingPercent = Math.max(100 - usedPercent, 0)
let resetSeconds: number | null = null let resetSeconds = secondsUntilUnixReset(info['reset_at'])
const resetTime = info['reset_time'] const resetTime = info['reset_time']
if (typeof resetTime === 'string' && resetTime.trim()) { if (typeof resetTime === 'string' && resetTime.trim()) {
const ts = Date.parse(resetTime.trim()) const ts = Date.parse(resetTime.trim())
@@ -247,11 +275,17 @@ const items = computed<QuotaItem[]>(() => {
} }
} }
result.push({ model, label: model, usedPercent, remainingPercent, resetSeconds }) result.push({
model,
label: resolveAntigravityQuotaLabel(model, info['display_name'], opaqueDisplayIndex),
usedPercent,
remainingPercent,
resetSeconds,
})
} }
result.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model)) result.sort(compareAntigravityQuotaItems)
return result return dedupeAntigravityQuotaItemsByLabel(result)
}) })
async function handleTestModel(modelName: string) { async function handleTestModel(modelName: string) {
@@ -361,31 +361,39 @@
/> />
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<ProviderQuotaProgressRow <ProviderQuotaProgressRow
v-for="group in getAntigravityQuotaSummaryForKey(key)" v-for="item in getAntigravityQuotaPreviewForKey(key)"
:key="group.key" :key="item.model"
:label="group.label" :label="item.label"
:used-percent="group.usedPercent" :title="item.model"
:remaining-percent="group.remainingPercent" :used-percent="item.usedPercent"
:meter-class="getQuotaRemainingClass(group.usedPercent)" :remaining-percent="item.remainingPercent"
:bar-class="getQuotaRemainingBarColor(group.usedPercent)" :meter-class="getQuotaRemainingClass(item.usedPercent)"
:bar-class="getQuotaRemainingBarColor(item.usedPercent)"
> >
<template #footer> <template #footer>
<div <div class="mt-0.5 space-y-0.5">
v-if="group.resetSeconds !== null || group.usedPercent > 0" <div
class="text-[9px] text-muted-foreground/70 mt-0.5" v-if="item.resetSeconds !== null"
> class="text-[9px] text-muted-foreground/70"
<template v-if="group.resetSeconds !== null && group.resetSeconds > 0"> >
{{ formatResetTime(group.resetSeconds) }}{{ legacyT('后重置') }} <template v-if="item.resetSeconds !== null && item.resetSeconds > 0">
</template> {{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
<template v-else-if="group.resetSeconds !== null && group.resetSeconds <= 0"> </template>
{{ legacyT('已重置') }} <template v-else-if="item.resetSeconds !== null && item.resetSeconds <= 0">
</template> {{ legacyT('已重置') }}
<template v-else> </template>
{{ legacyT('重置时间未知') }} </div>
</template>
</div> </div>
</template> </template>
</ProviderQuotaProgressRow> </ProviderQuotaProgressRow>
<button
v-if="getAntigravityQuotaHiddenCountForKey(key) > 0"
type="button"
class="col-span-2 text-left text-[10px] text-muted-foreground hover:text-foreground transition-colors"
@click="openAntigravityQuotaDialog(key)"
>
{{ legacyT('另有') }} {{ getAntigravityQuotaHiddenCountForKey(key) }} {{ legacyT('个模型,查看全部') }}
</button>
</div> </div>
</template> </template>
</div> </div>
@@ -949,6 +957,11 @@ import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMo
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue' import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue' import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes' import { useProxyNodesStore } from '@/stores/proxy-nodes'
import {
compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
import { import {
deleteEndpointKey, deleteEndpointKey,
recoverKeyHealth, recoverKeyHealth,
@@ -3348,53 +3361,72 @@ function secondsUntilReset(resetTime: string): number | null {
return diff > 0 ? diff : 0 return diff > 0 ? diff : 0
} }
function secondsUntilUnixReset(resetAt: number | string | null | undefined): number | null {
const numericResetAt = Number(resetAt)
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
const now = Math.floor(Date.now() / 1000)
return Math.max(Math.floor(numericResetAt - now), 0)
}
function coerceAntigravityPercent(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 100)
}
function coerceAntigravityRemainingFraction(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 1)
}
function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] { function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] {
const quotaByModel = metadata?.antigravity?.quota_by_model const quotaByModel = metadata?.antigravity?.quota_by_model
if (!quotaByModel || typeof quotaByModel !== 'object') return [] if (!quotaByModel || typeof quotaByModel !== 'object') return []
const items: AntigravityQuotaItem[] = [] const items: AntigravityQuotaItem[] = []
const opaqueDisplayIndex = { value: 1 }
for (const [model, rawInfo] of Object.entries(quotaByModel)) { for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue if (!model) continue
const info: Partial<AntigravityModelQuota> = rawInfo || {} const info: Partial<AntigravityModelQuota> = rawInfo || {}
let usedPercent = Number(info.used_percent) let usedPercent = coerceAntigravityPercent(info.used_percent)
if (!Number.isFinite(usedPercent)) { if (usedPercent === undefined) {
const remainingFraction = Number(info.remaining_fraction) const remainingFraction = coerceAntigravityRemainingFraction(info.remaining_fraction)
if (Number.isFinite(remainingFraction)) { if (remainingFraction !== undefined) {
usedPercent = (1 - remainingFraction) * 100 usedPercent = (1 - remainingFraction) * 100
} else { } else {
continue continue
} }
} }
if (usedPercent < 0) usedPercent = 0 usedPercent = coerceAntigravityPercent(usedPercent) ?? 0
if (usedPercent > 100) usedPercent = 100
const remainingPercent = Math.max(100 - usedPercent, 0) const remainingPercent = Math.max(100 - usedPercent, 0)
let resetSeconds: number | null = null let resetSeconds = secondsUntilUnixReset(info.reset_at)
if (typeof info.reset_time === 'string' && info.reset_time.trim()) { if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
resetSeconds = secondsUntilReset(info.reset_time.trim()) resetSeconds = secondsUntilReset(info.reset_time.trim()) ?? resetSeconds
} }
items.push({ items.push({
model, model,
label: model, label: resolveAntigravityQuotaLabel(model, info.display_name, opaqueDisplayIndex),
usedPercent, usedPercent,
remainingPercent, remainingPercent,
resetSeconds, resetSeconds,
}) })
} }
// 按“最紧张”(已用最多)优先排序,便于快速定位额度风险;完整列表通过滚动展示 items.sort(compareAntigravityQuotaItems)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model)) return dedupeAntigravityQuotaItemsByLabel(items)
return items
} }
function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQuotaItem[] { function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQuotaItem[] {
const quota = getQuotaSnapshotForProvider(key, 'antigravity') const quota = getQuotaSnapshotForProvider(key, 'antigravity')
const windows = getQuotaWindowByScope(quota, 'model') const windows = getQuotaWindowByScope(quota, 'model')
if (!quota || windows.length === 0) return [] if (!quota || windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
const items = windows const items = windows
.map((window) => { .map((window) => {
@@ -3418,7 +3450,11 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
return { return {
model, model,
label: String(window.label || window.model || model), label: resolveAntigravityQuotaLabel(
model,
window.label || window.model,
opaqueDisplayIndex,
),
usedPercent: normalizedUsedPercent, usedPercent: normalizedUsedPercent,
remainingPercent: normalizedRemainingPercent, remainingPercent: normalizedRemainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window), resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
@@ -3426,124 +3462,24 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
}) })
.filter((item): item is AntigravityQuotaItem => item !== null) .filter((item): item is AntigravityQuotaItem => item !== null)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model)) items.sort(compareAntigravityQuotaItems)
return items return dedupeAntigravityQuotaItemsByLabel(items)
} }
// Antigravity 配额分组定义(按匹配优先级排列,具体规则在前) const ANTIGRAVITY_QUOTA_PREVIEW_LIMIT = 6
interface AntigravityQuotaGroup {
key: string
label: string
match: (model: string) => boolean
}
const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroup[] = [ function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
{ key: 'claude', label: 'Claude', match: m => m.includes('claude') },
{ key: 'gemini-2.5', label: 'Gemini 2.5', match: m => m.includes('gemini-2.5') || m.includes('gemini-2-5') },
{ key: 'gemini-3', label: 'Gemini 3', match: m => m.includes('gemini-3') && !m.includes('image') },
{ key: 'gemini-3-image', label: 'Gemini 3 Image', match: m => m.includes('gemini-3') && m.includes('image') },
]
interface AntigravityQuotaSummaryItem {
key: string
label: string
usedPercent: number // 组内最高已用百分比(最紧张)
remainingPercent: number // 100 - usedPercent
resetSeconds: number | null
}
function getAntigravityQuotaSummary(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaSummaryItem[] {
const items = getAntigravityQuotaItems(metadata)
if (!items.length) return []
// 将每个模型归入分组
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>()
for (const item of items) {
const model = item.model.toLowerCase()
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model))
if (!group) continue
const existing = groupMap.get(group.key)
if (!existing) {
groupMap.set(group.key, {
label: group.label,
maxUsed: item.usedPercent,
resetSeconds: item.resetSeconds,
})
} else {
if (item.usedPercent > existing.maxUsed) {
existing.maxUsed = item.usedPercent
}
if (existing.resetSeconds === null) {
existing.resetSeconds = item.resetSeconds
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
existing.resetSeconds = item.resetSeconds
}
}
}
// 按 ANTIGRAVITY_QUOTA_GROUPS 定义的顺序输出
const result: AntigravityQuotaSummaryItem[] = []
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
const data = groupMap.get(group.key)
if (!data) continue
result.push({
key: group.key,
label: data.label,
usedPercent: data.maxUsed,
remainingPercent: Math.max(100 - data.maxUsed, 0),
resetSeconds: data.resetSeconds,
})
}
return result
}
function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaSummaryItem[] {
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key) const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
if (snapshotItems.length > 0) { if (snapshotItems.length > 0) return snapshotItems
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>() return getAntigravityQuotaItems(key.upstream_metadata)
}
for (const item of snapshotItems) { function getAntigravityQuotaPreviewForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
const model = item.model.toLowerCase() return getAntigravityQuotaItemsForKey(key).slice(0, ANTIGRAVITY_QUOTA_PREVIEW_LIMIT)
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model)) }
if (!group) continue
const existing = groupMap.get(group.key) function getAntigravityQuotaHiddenCountForKey(key: EndpointAPIKey): number {
if (!existing) { return Math.max(getAntigravityQuotaItemsForKey(key).length - ANTIGRAVITY_QUOTA_PREVIEW_LIMIT, 0)
groupMap.set(group.key, {
label: group.label,
maxUsed: item.usedPercent,
resetSeconds: item.resetSeconds,
})
} else {
if (item.usedPercent > existing.maxUsed) {
existing.maxUsed = item.usedPercent
}
if (existing.resetSeconds === null) {
existing.resetSeconds = item.resetSeconds
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
existing.resetSeconds = item.resetSeconds
}
}
}
const result: AntigravityQuotaSummaryItem[] = []
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
const data = groupMap.get(group.key)
if (!data) continue
result.push({
key: group.key,
label: data.label,
usedPercent: data.maxUsed,
remainingPercent: Math.max(100 - data.maxUsed, 0),
resetSeconds: data.resetSeconds,
})
}
return result
}
return getAntigravityQuotaSummary(key.upstream_metadata)
} }
function getResetCountdownText( function getResetCountdownText(
@@ -0,0 +1,207 @@
import { describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h } from 'vue'
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
import type { UpstreamMetadata } from '@/api/endpoints/types'
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
const passthrough = (name: string) => defineComponent({
name,
setup(_, { slots }) {
return () => h('div', [
slots.headerActions?.(),
slots.default?.(),
slots.footer?.(),
])
},
})
return {
Dialog: passthrough('DialogStub'),
DropdownMenu: passthrough('DropdownMenuStub'),
DropdownMenuTrigger: passthrough('DropdownMenuTriggerStub'),
DropdownMenuContent: passthrough('DropdownMenuContentStub'),
DropdownMenuItem: defineComponent({
name: 'DropdownMenuItemStub',
emits: ['select'],
setup(_, { emit, slots }) {
return () => h('button', { type: 'button', onClick: () => emit('select') }, slots.default?.())
},
}),
}
})
vi.mock('@/components/ui/button.vue', async () => {
const { defineComponent, h } = await import('vue')
return {
default: defineComponent({
name: 'ButtonStub',
setup(_, { attrs, slots }) {
return () => h('button', { ...attrs, type: 'button' }, slots.default?.())
},
}),
}
})
vi.mock('lucide-vue-next', async () => {
const { defineComponent, h } = await import('vue')
const Icon = defineComponent({
name: 'IconStub',
setup() {
return () => h('span')
},
})
return {
BarChart3: Icon,
Loader2: Icon,
Play: Icon,
}
})
vi.mock('@/api/endpoints/providers', () => ({
testModel: vi.fn(),
}))
vi.mock('@/composables/useToast', () => ({
useToast: () => ({
error: vi.fn(),
success: vi.fn(),
}),
}))
vi.mock('@/utils/errorParser', () => ({
parseApiError: (value: unknown) => String(value),
}))
function mount(metadata: UpstreamMetadata) {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(defineComponent({
setup() {
return () => h(AntigravityQuotaDialog, {
open: true,
metadata,
keyName: 'Key-1',
})
},
}))
app.mount(root)
return {
root,
unmount: () => {
app.unmount()
root.remove()
},
}
}
describe('AntigravityQuotaDialog', () => {
it('renders opaque quota identifiers with concise visible labels', () => {
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
[rawIdentifier]: {
display_name: rawIdentifier,
remaining_fraction: 0.25,
used_percent: 75,
},
},
},
})
expect(root.textContent).toContain('Key-1')
expect(root.textContent).not.toContain(rawIdentifier)
expect(root.textContent).toContain('25.0%')
unmount()
})
it('orders important Gemini and Claude quota rows before low-priority rows', () => {
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
tab_flash_lite_preview: {
display_name: 'Tab Flash Lite Preview',
remaining_fraction: 0.01,
used_percent: 99,
},
'gemini-3-flash-agent': {
display_name: 'Gemini 3.5 Flash (High)',
remaining_fraction: 0.9,
used_percent: 10,
},
'gemini-pro-agent': {
display_name: 'gemini-pro-agent',
remaining_fraction: 0.95,
used_percent: 5,
},
'gemini-3.5-flash-low': {
display_name: 'Gemini 3.5 Flash (Medium)',
remaining_fraction: 0.8,
used_percent: 20,
},
'claude-opus-4-6-thinking': {
display_name: 'Claude Opus 4.6 (Thinking)',
remaining_fraction: 1,
used_percent: 0,
},
chat_20706: {
display_name: 'chat_20706',
remaining_fraction: 0,
used_percent: 100,
},
},
},
})
const text = root.textContent || ''
expect(text).not.toContain('gemini-pro-agent')
expect(text).not.toContain('claude-opus-4-6-thinking')
expect(text.indexOf('Claude Opus 4.6 (Thinking)')).toBeLessThan(text.indexOf('Gemini 3.1 Pro (High)'))
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)'))
expect(text.indexOf('Gemini 3.5 Flash (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (Medium)'))
expect(text.indexOf('Gemini 3.5 Flash (Medium)')).toBeLessThan(text.indexOf('Tab Flash Lite Preview'))
expect(text.indexOf('Tab Flash Lite Preview')).toBeLessThan(text.indexOf('chat_20706'))
unmount()
})
it('renders one row for duplicate quota labels and keeps the preferred active bucket', () => {
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
'gemini-3.1-pro-high': {
display_name: 'Gemini 3.1 Pro (High)',
remaining_fraction: 0.95,
used_percent: 5,
},
'gemini-pro-agent': {
display_name: 'Gemini 3.1 Pro (High)',
remaining_fraction: 0.4,
used_percent: 60,
},
'gemini-3-flash-agent': {
display_name: 'Gemini 3.5 Flash (High)',
remaining_fraction: 0.9,
used_percent: 10,
},
},
},
})
const text = root.textContent || ''
expect(text.match(/Gemini 3\.1 Pro \(High\)/g)).toHaveLength(1)
expect(text).toContain('40.0%')
expect(text).not.toContain('95.0%')
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)'))
unmount()
})
})
@@ -5,6 +5,7 @@ import { getDefaultEndpointBaseUrl, getDefaultEndpointPath } from '../endpoint-d
const apiFormats = [ const apiFormats = [
{ value: 'openai:chat', default_path: '/v1/chat/completions' }, { value: 'openai:chat', default_path: '/v1/chat/completions' },
{ value: 'gemini:generate_content', default_path: '/v1beta/models/{model}:{action}' }, { value: 'gemini:generate_content', default_path: '/v1beta/models/{model}:{action}' },
{ value: 'gemini:interactions', default_path: '/v1/interactions' },
{ value: 'gemini:embedding', default_path: '/v1beta/models/{model}:embedContent' }, { value: 'gemini:embedding', default_path: '/v1beta/models/{model}:embedContent' },
{ value: 'gemini:video', default_path: '/v1beta/models/{model}:predictLongRunning' }, { value: 'gemini:video', default_path: '/v1beta/models/{model}:predictLongRunning' },
{ value: 'openai:responses', default_path: '/v1/responses' }, { value: 'openai:responses', default_path: '/v1/responses' },
@@ -31,6 +32,12 @@ describe('endpoint default paths', () => {
apiFormats, apiFormats,
})).toBe('/models/{model}:embedContent') })).toBe('/models/{model}:embedContent')
expect(getDefaultEndpointPath({
apiFormat: 'gemini:interactions',
providerType: 'custom',
apiFormats,
})).toBe('/interactions')
expect(getDefaultEndpointPath({ expect(getDefaultEndpointPath({
apiFormat: 'gemini:video', apiFormat: 'gemini:video',
providerType: 'custom', providerType: 'custom',
@@ -218,6 +225,11 @@ describe('endpoint default paths', () => {
baseUrl: 'https://generativelanguage.googleapis.com', baseUrl: 'https://generativelanguage.googleapis.com',
})).toBe('https://generativelanguage.googleapis.com/v1beta') })).toBe('https://generativelanguage.googleapis.com/v1beta')
expect(getDefaultEndpointBaseUrl({
apiFormat: 'gemini:interactions',
baseUrl: 'https://generativelanguage.googleapis.com',
})).toBe('https://generativelanguage.googleapis.com/v1')
expect(getDefaultEndpointBaseUrl({ expect(getDefaultEndpointBaseUrl({
apiFormat: 'gemini:video', apiFormat: 'gemini:video',
baseUrl: 'https://generativelanguage.googleapis.com', baseUrl: 'https://generativelanguage.googleapis.com',
@@ -98,11 +98,17 @@ function usesVersionedApiRootByDefault(apiFormat: string): boolean {
|| apiFormat === 'jina:rerank' || apiFormat === 'jina:rerank'
|| apiFormat === 'claude:messages' || apiFormat === 'claude:messages'
|| apiFormat === 'gemini:generate_content' || apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:interactions'
|| apiFormat === 'gemini:embedding' || apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video' || apiFormat === 'gemini:video'
} }
function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' { function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
if (
apiFormat === 'gemini:interactions'
) {
return '/v1'
}
if ( if (
apiFormat === 'gemini:generate_content' apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:embedding' || apiFormat === 'gemini:embedding'
@@ -116,6 +122,7 @@ function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
function skipsVersionedApiRootDefault(apiFormat: string, baseUrl: string): boolean { function skipsVersionedApiRootDefault(apiFormat: string, baseUrl: string): boolean {
if ( if (
apiFormat === 'gemini:generate_content' apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:interactions'
|| apiFormat === 'gemini:embedding' || apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video' || apiFormat === 'gemini:video'
) { ) {
@@ -86,6 +86,17 @@ describe('buildDefaultModelTestRequestBody', () => {
expect(body.messages).toBeUndefined() expect(body.messages).toBeUndefined()
}) })
it('uses Gemini Interactions payloads for interaction endpoints', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody('gemini-3.5-flash', 'gemini:interactions'))
expect(body).toEqual({
model: 'gemini-3.5-flash',
input: 'Hello! This is a test message.',
stream: true,
})
expect(body.messages).toBeUndefined()
})
it('uses image generation tools for image models on OpenAI Responses endpoints', () => { it('uses image generation tools for image models on OpenAI Responses endpoints', () => {
const body = JSON.parse(buildDefaultModelTestRequestBody( const body = JSON.parse(buildDefaultModelTestRequestBody(
'gpt-image-2', 'gpt-image-2',
@@ -196,6 +196,14 @@ export function buildDefaultModelTestRequestBody(
}, null, 2) }, null, 2)
} }
if (normalizedApiFormat === 'gemini:interactions') {
return JSON.stringify({
model: modelName,
input: DEFAULT_MODEL_TEST_MESSAGE,
stream: true,
}, null, 2)
}
if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) { if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) {
return JSON.stringify({ return JSON.stringify({
model: modelName, model: modelName,
@@ -12,6 +12,7 @@ const ENDPOINT_SORT_ORDER = [
'openai:embedding', 'openai:embedding',
'openai:rerank', 'openai:rerank',
'gemini:generate_content', 'gemini:generate_content',
'gemini:interactions',
'gemini:embedding', 'gemini:embedding',
'openai:video', 'openai:video',
'gemini:video', 'gemini:video',
@@ -32,6 +32,7 @@ export function useProviderFilters(
{ value: 'openai:embedding', label: 'OpenAI Embedding' }, { value: 'openai:embedding', label: 'OpenAI Embedding' },
{ value: 'openai:rerank', label: 'OpenAI Rerank' }, { value: 'openai:rerank', label: 'OpenAI Rerank' },
{ value: 'gemini:generate_content', label: 'Gemini Generate Content' }, { value: 'gemini:generate_content', label: 'Gemini Generate Content' },
{ value: 'gemini:interactions', label: 'Gemini Interactions' },
{ value: 'gemini:embedding', label: 'Gemini Embedding' }, { value: 'gemini:embedding', label: 'Gemini Embedding' },
{ value: 'jina:embedding', label: 'Jina Embedding' }, { value: 'jina:embedding', label: 'Jina Embedding' },
{ value: 'jina:rerank', label: 'Jina Rerank' }, { value: 'jina:rerank', label: 'Jina Rerank' },
@@ -0,0 +1,175 @@
import { describe, expect, it } from 'vitest'
import {
compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
describe('antigravityQuota', () => {
it('uses canonical labels from model ids before stale upstream labels', () => {
const opaqueDisplayIndex = { value: 1 }
expect(
resolveAntigravityQuotaLabel(
'gemini-3-flash-agent',
'Gemini 3.5 Flash (High)',
opaqueDisplayIndex,
),
).toBe('Gemini 3.5 Flash (High)')
expect(
resolveAntigravityQuotaLabel(
'gemini-3.5-flash-low',
'Gemini 3.5 Flash (Medium)',
opaqueDisplayIndex,
),
).toBe('Gemini 3.5 Flash (Medium)')
expect(
resolveAntigravityQuotaLabel(
'gemini-3.5-flash-extra-low',
'Gemini 3.5 Flash (Low)',
opaqueDisplayIndex,
),
).toBe('Gemini 3.5 Flash (Low)')
expect(
resolveAntigravityQuotaLabel(
'gemini-pro-agent',
'gemini-pro-agent',
opaqueDisplayIndex,
),
).toBe('Gemini 3.1 Pro (High)')
expect(
resolveAntigravityQuotaLabel(
'gemini-3.1-pro-high',
'gemini-3.1-pro-high',
opaqueDisplayIndex,
),
).toBe('Gemini 3.1 Pro (High)')
expect(
resolveAntigravityQuotaLabel(
'gemini-2.5-flash',
'Gemini 3.1 Flash Lite',
opaqueDisplayIndex,
),
).toBe('Gemini 3.1 Flash Lite')
expect(
resolveAntigravityQuotaLabel(
'claude-sonnet-4-6',
'Claude Sonnet 4.6 (Thinking)',
opaqueDisplayIndex,
),
).toBe('Claude Sonnet 4.6 (Thinking)')
expect(
resolveAntigravityQuotaLabel(
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb',
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb',
opaqueDisplayIndex,
),
).toBe('Key-1')
})
it('keeps latest Gemini and Claude quota buckets before low-priority buckets', () => {
const items = [
{
model: 'tab_flash_lite_preview',
label: 'Tab Flash Lite Preview',
remainingPercent: 1,
resetSeconds: 10,
},
{
model: 'gemini-3.5-flash-extra-low',
label: 'Gemini 3.5 Flash (Low)',
remainingPercent: 95,
resetSeconds: null,
},
{
model: 'gemini-3.5-flash-low',
label: 'Gemini 3.5 Flash (Medium)',
remainingPercent: 90,
resetSeconds: null,
},
{
model: 'gemini-3-flash-agent',
label: 'Gemini 3.5 Flash (High)',
remainingPercent: 80,
resetSeconds: null,
},
{
model: 'gemini-pro-agent',
label: 'Gemini 3.1 Pro (High)',
remainingPercent: 70,
resetSeconds: null,
},
{
model: 'chat_20706',
label: 'chat_20706',
remainingPercent: 0,
resetSeconds: 1,
},
{
model: 'claude-opus-4-6-thinking',
label: 'Claude Opus 4.6 (Thinking)',
remainingPercent: 100,
resetSeconds: null,
},
{
model: 'gemini-2.5-flash-lite',
label: 'Gemini 3.1 Flash Lite',
remainingPercent: 5,
resetSeconds: 5,
},
].sort(compareAntigravityQuotaItems)
expect(items.map(item => item.model)).toEqual([
'claude-opus-4-6-thinking',
'gemini-pro-agent',
'gemini-3-flash-agent',
'gemini-3.5-flash-low',
'gemini-3.5-flash-extra-low',
'gemini-2.5-flash-lite',
'tab_flash_lite_preview',
'chat_20706',
])
})
it('deduplicates display labels while keeping the current preferred quota bucket', () => {
const items = dedupeAntigravityQuotaItemsByLabel([
{
model: 'gemini-3.1-pro-high',
label: 'Gemini 3.1 Pro (High)',
remainingPercent: 95,
resetSeconds: null,
},
{
model: 'gemini-pro-agent',
label: 'Gemini 3.1 Pro (High)',
remainingPercent: 40,
resetSeconds: null,
},
{
model: 'claude-sonnet-4-6-thinking',
label: 'Claude Sonnet 4.6 (Thinking)',
remainingPercent: 95,
resetSeconds: null,
},
{
model: 'claude-sonnet-4-6',
label: 'Claude Sonnet 4.6 (Thinking)',
remainingPercent: 40,
resetSeconds: null,
},
{
model: 'gemini-3-flash-agent',
label: 'Gemini 3.5 Flash (High)',
remainingPercent: 80,
resetSeconds: null,
},
])
expect(items.map(item => item.model)).toEqual([
'claude-sonnet-4-6',
'gemini-pro-agent',
'gemini-3-flash-agent',
])
})
})
@@ -0,0 +1,117 @@
export interface AntigravityQuotaSortableItem {
model: string
label: string
remainingPercent: number
resetSeconds: number | null
}
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
'gemini-pro-agent': 'Gemini 3.1 Pro (High)',
'gemini-3.1-pro-high': 'Gemini 3.1 Pro (High)',
'gemini-3.1-pro-low': 'Gemini 3.1 Pro (Low)',
'gemini-3-flash-agent': 'Gemini 3.5 Flash (High)',
'gemini-3.5-flash-low': 'Gemini 3.5 Flash (Medium)',
'gemini-3.5-flash-extra-low': 'Gemini 3.5 Flash (Low)',
'claude-opus-4-6-thinking': 'Claude Opus 4.6 (Thinking)',
'claude-sonnet-4-6': 'Claude Sonnet 4.6 (Thinking)',
'claude-sonnet-4-6-thinking': 'Claude Sonnet 4.6 (Thinking)',
'gemini-3.1-flash-image': 'Gemini 3.1 Flash Image',
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
'gemini-3-flash': 'Gemini 3 Flash',
'gemini-2.5-pro': 'Gemini 2.5 Pro',
'gemini-2.5-flash-thinking': 'Gemini 3.1 Flash Lite',
'gemini-2.5-flash': 'Gemini 3.1 Flash Lite',
'gemini-2.5-flash-lite': 'Gemini 3.1 Flash Lite',
'gpt-oss-120b-medium': 'GPT-OSS 120B (Medium)',
'tab_flash_lite_preview': 'Tab Flash Lite Preview',
'tab_jump_flash_lite_preview': 'Tab Jump Flash Lite Preview',
'models/proactive-observer': 'Proactive Observer',
}
const ANTIGRAVITY_MODEL_PRIORITY: Record<string, number> = {
'claude-opus-4-6-thinking': 10,
'claude-sonnet-4-6': 20,
'claude-sonnet-4-6-thinking': 25,
'gemini-pro-agent': 30,
'gemini-3.1-pro-high': 35,
'gemini-3.1-pro-low': 40,
'gemini-3-flash-agent': 50,
'gemini-3.5-flash-low': 60,
'gemini-3.5-flash-extra-low': 70,
'gemini-3.1-flash-image': 80,
'gemini-3.1-flash-lite': 90,
'gemini-3-flash': 180,
'gemini-2.5-pro': 300,
'gemini-2.5-flash-thinking': 310,
'gemini-2.5-flash': 320,
'gemini-2.5-flash-lite': 330,
'gpt-oss-120b-medium': 700,
'models/proactive-observer': 780,
'tab_flash_lite_preview': 800,
'tab_jump_flash_lite_preview': 810,
}
export function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
export function resolveAntigravityQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const normalizedModel = model.trim()
const canonical = ANTIGRAVITY_MODEL_LABELS[normalizedModel]
if (canonical) return canonical
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(normalizedModel) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || normalizedModel
}
function getAntigravityModelPriority(model: string): number {
const normalizedModel = model.trim()
const explicit = ANTIGRAVITY_MODEL_PRIORITY[normalizedModel]
if (explicit !== undefined) return explicit
if (normalizedModel.startsWith('claude-')) return 30
if (normalizedModel.startsWith('gemini-3.')) return 200
if (normalizedModel.startsWith('gemini-2.')) return 390
if (normalizedModel.startsWith('gemini-')) return 490
if (normalizedModel.startsWith('gpt-oss-')) return 700
if (normalizedModel.startsWith('models/')) return 780
if (normalizedModel.startsWith('tab_')) return 800
if (normalizedModel.startsWith('chat_')) return 900
if (isOpaqueAntigravityQuotaIdentifier(normalizedModel)) return 950
return 850
}
export function compareAntigravityQuotaItems<T extends AntigravityQuotaSortableItem>(
a: T,
b: T,
): number {
return (getAntigravityModelPriority(a.model) - getAntigravityModelPriority(b.model))
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| (a.remainingPercent - b.remainingPercent)
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}
export function dedupeAntigravityQuotaItemsByLabel<T extends AntigravityQuotaSortableItem>(
items: T[],
): T[] {
const selectedByLabel = new Map<string, T>()
for (const item of items) {
const label = item.label.trim()
if (!label) continue
const selected = selectedByLabel.get(label)
if (!selected || compareAntigravityQuotaItems(item, selected) < 0) {
selectedByLabel.set(label, item)
}
}
return Array.from(selectedByLabel.values()).sort(compareAntigravityQuotaItems)
}
+1
View File
@@ -1192,6 +1192,7 @@ export const MOCK_API_FORMATS = {
{ value: 'openai:image', label: 'OpenAI Image', default_path: '/v1/images/generations', aliases: [] }, { value: 'openai:image', label: 'OpenAI Image', default_path: '/v1/images/generations', aliases: [] },
{ value: 'openai:video', label: 'OpenAI Video', default_path: '/v1/videos', aliases: [] }, { value: 'openai:video', label: 'OpenAI Video', default_path: '/v1/videos', aliases: [] },
{ value: 'gemini:generate_content', label: 'Gemini Generate Content', default_path: '/v1beta/models/{model}:{action}', aliases: [] }, { value: 'gemini:generate_content', label: 'Gemini Generate Content', default_path: '/v1beta/models/{model}:{action}', aliases: [] },
{ value: 'gemini:interactions', label: 'Gemini Interactions', default_path: '/v1/interactions', aliases: [] },
{ value: 'gemini:embedding', label: 'Gemini Embedding', default_path: '/v1beta/models/{model}:embedContent', aliases: [] }, { value: 'gemini:embedding', label: 'Gemini Embedding', default_path: '/v1beta/models/{model}:embedContent', aliases: [] },
{ value: 'gemini:video', label: 'Gemini Video', default_path: '/v1beta/models/{model}:predictLongRunning', aliases: [] }, { value: 'gemini:video', label: 'Gemini Video', default_path: '/v1beta/models/{model}:predictLongRunning', aliases: [] },
{ value: 'jina:embedding', label: 'Jina Embedding', default_path: '/v1/embeddings', aliases: [] }, { value: 'jina:embedding', label: 'Jina Embedding', default_path: '/v1/embeddings', aliases: [] },
+1
View File
@@ -533,6 +533,7 @@ function mockApiFormatDisplayName(apiFormat: string) {
const labels: Record<string, string> = { const labels: Record<string, string> = {
'claude:messages': 'Claude Messages', 'claude:messages': 'Claude Messages',
'gemini:generate_content': 'Gemini Generate Content', 'gemini:generate_content': 'Gemini Generate Content',
'gemini:interactions': 'Gemini Interactions',
'openai:chat': 'OpenAI Chat', 'openai:chat': 'OpenAI Chat',
'openai:embedding': 'OpenAI Embedding' 'openai:embedding': 'OpenAI Embedding'
} }