mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
Merge branch 'fawney19:main' into main
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+306
-1
@@ -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,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+186
-97
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
"a_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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
@@ -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 $$;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -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'
|
||||||
) {
|
) {
|
||||||
|
|||||||
+11
@@ -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)
|
||||||
|
}
|
||||||
@@ -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: [] },
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user