mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 03:09:50 +08:00
Merge commit 'refs/pr/585'
# Conflicts: # apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs # apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs # apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs # apps/aether-gateway/src/tests/control/admin/provider_query.rs # crates/aether-provider-transport/src/gemini_cli/mod.rs # crates/aether-provider-transport/src/gemini_cli/request.rs # crates/aether-provider-transport/src/gemini_cli/url.rs # crates/aether-provider-transport/src/lib.rs
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
pub const GEMINI_CLI_PROVIDER_TYPE: &str = "gemini_cli";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct GeminiCliRequestAuth {
|
||||
pub project_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestAuthSupport {
|
||||
Supported(GeminiCliRequestAuth),
|
||||
Unsupported(GeminiCliRequestAuthUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum GeminiCliRequestAuthUnsupportedReason {
|
||||
WrongProviderType,
|
||||
InvalidAuthConfigJson,
|
||||
}
|
||||
|
||||
pub fn is_gemini_cli_provider_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||
transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(GEMINI_CLI_PROVIDER_TYPE)
|
||||
}
|
||||
|
||||
pub fn resolve_local_gemini_cli_request_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> GeminiCliRequestAuthSupport {
|
||||
if !is_gemini_cli_provider_transport(transport) {
|
||||
return GeminiCliRequestAuthSupport::Unsupported(
|
||||
GeminiCliRequestAuthUnsupportedReason::WrongProviderType,
|
||||
);
|
||||
}
|
||||
|
||||
let metadata = transport.key.upstream_metadata.as_ref();
|
||||
let metadata_project_id = metadata.and_then(resolve_project_id_from_value);
|
||||
let metadata_session_id = metadata.and_then(resolve_session_id_from_value);
|
||||
|
||||
let Some(raw_auth_config) = transport
|
||||
.key
|
||||
.decrypted_auth_config
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: metadata_project_id,
|
||||
session_id: metadata_session_id,
|
||||
});
|
||||
};
|
||||
|
||||
let Ok(auth_config) = serde_json::from_str::<Value>(raw_auth_config) else {
|
||||
return GeminiCliRequestAuthSupport::Unsupported(
|
||||
GeminiCliRequestAuthUnsupportedReason::InvalidAuthConfigJson,
|
||||
);
|
||||
};
|
||||
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: metadata_project_id.or_else(|| resolve_project_id_from_value(&auth_config)),
|
||||
session_id: metadata_session_id.or_else(|| resolve_session_id_from_value(&auth_config)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_gemini_cli_project_id(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Option<String> {
|
||||
match resolve_local_gemini_cli_request_auth(transport) {
|
||||
GeminiCliRequestAuthSupport::Supported(auth) => auth.project_id,
|
||||
GeminiCliRequestAuthSupport::Unsupported(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_project_id_from_value(value: &Value) -> Option<String> {
|
||||
find_string_by_paths(
|
||||
value,
|
||||
&[
|
||||
&["project"],
|
||||
&["project_id"],
|
||||
&["projectId"],
|
||||
&["project", "id"],
|
||||
&["project", "project_id"],
|
||||
&["project", "projectId"],
|
||||
&["cloudaicompanionProject"],
|
||||
&["cloudaicompanionProject", "id"],
|
||||
&["cloudaicompanion_project"],
|
||||
&["cloudaicompanion_project", "id"],
|
||||
&["gemini_cli", "project"],
|
||||
&["gemini_cli", "project_id"],
|
||||
&["gemini_cli", "projectId"],
|
||||
&["gemini_cli", "cloudaicompanionProject"],
|
||||
&["gemini_cli", "cloudaicompanionProject", "id"],
|
||||
&["geminiCli", "project"],
|
||||
&["geminiCli", "project_id"],
|
||||
&["geminiCli", "projectId"],
|
||||
&["metadata", "project"],
|
||||
&["metadata", "project_id"],
|
||||
&["metadata", "projectId"],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_session_id_from_value(value: &Value) -> Option<String> {
|
||||
find_string_by_paths(
|
||||
value,
|
||||
&[
|
||||
&["session_id"],
|
||||
&["sessionId"],
|
||||
&["gemini_cli", "session_id"],
|
||||
&["gemini_cli", "sessionId"],
|
||||
&["geminiCli", "session_id"],
|
||||
&["geminiCli", "sessionId"],
|
||||
&["metadata", "session_id"],
|
||||
&["metadata", "sessionId"],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn find_string_by_paths(value: &Value, paths: &[&[&str]]) -> Option<String> {
|
||||
for path in paths {
|
||||
let mut current = value;
|
||||
let mut matched = true;
|
||||
for segment in *path {
|
||||
let Some(next) = current.get(*segment) else {
|
||||
matched = false;
|
||||
break;
|
||||
};
|
||||
current = next;
|
||||
}
|
||||
if !matched {
|
||||
continue;
|
||||
}
|
||||
if let Some(string) = current
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
{
|
||||
return Some(string.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
resolve_gemini_cli_project_id, resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth,
|
||||
GeminiCliRequestAuthSupport,
|
||||
};
|
||||
use crate::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
|
||||
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-1".to_string(),
|
||||
name: "Gemini CLI".to_string(),
|
||||
provider_type: "gemini_cli".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: true,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
api_format: "gemini:generate_content".to_string(),
|
||||
api_family: None,
|
||||
endpoint_kind: None,
|
||||
is_active: true,
|
||||
base_url: "https://cloudcode-pa.googleapis.com".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: None,
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "oauth".to_string(),
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
upstream_metadata: None,
|
||||
decrypted_api_key: "__oauth__".to_string(),
|
||||
decrypted_auth_config: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_project_and_session_metadata_when_available() {
|
||||
let mut transport = sample_transport();
|
||||
transport.key.decrypted_auth_config = Some(
|
||||
r#"{
|
||||
"cloudaicompanionProject": {"id": "project-123"},
|
||||
"metadata": {"sessionId": "session-123"}
|
||||
}"#
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_local_gemini_cli_request_auth(&transport),
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: Some("project-123".to_string()),
|
||||
session_id: Some("session-123".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_id_prefers_upstream_metadata_then_auth_config() {
|
||||
let mut transport = sample_transport();
|
||||
transport.key.upstream_metadata = Some(serde_json::json!({
|
||||
"gemini_cli": {
|
||||
"project_id": "metadata-project"
|
||||
}
|
||||
}));
|
||||
transport.key.decrypted_auth_config = Some(r#"{"project_id":"auth-project"}"#.to_string());
|
||||
|
||||
assert_eq!(
|
||||
resolve_gemini_cli_project_id(&transport).as_deref(),
|
||||
Some("metadata-project")
|
||||
);
|
||||
|
||||
transport.key.upstream_metadata = None;
|
||||
assert_eq!(
|
||||
resolve_gemini_cli_project_id(&transport).as_deref(),
|
||||
Some("auth-project")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_metadata_still_supports_request_envelope() {
|
||||
let transport = sample_transport();
|
||||
|
||||
assert_eq!(
|
||||
resolve_local_gemini_cli_request_auth(&transport),
|
||||
GeminiCliRequestAuthSupport::Supported(GeminiCliRequestAuth {
|
||||
project_id: None,
|
||||
session_id: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
mod auth;
|
||||
mod request;
|
||||
mod url;
|
||||
|
||||
pub use auth::{
|
||||
is_gemini_cli_provider_transport, resolve_gemini_cli_project_id,
|
||||
resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth, GeminiCliRequestAuthSupport,
|
||||
GeminiCliRequestAuthUnsupportedReason, GEMINI_CLI_PROVIDER_TYPE,
|
||||
};
|
||||
pub use request::{
|
||||
build_gemini_cli_v1internal_request, resolve_gemini_cli_project_id,
|
||||
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
|
||||
GeminiCliRequestEnvelopeSupport, GeminiCliRequestEnvelopeUnsupportedReason,
|
||||
};
|
||||
pub use url::{
|
||||
@@ -11,15 +17,4 @@ pub use url::{
|
||||
GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE,
|
||||
};
|
||||
|
||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
pub const GEMINI_CLI_PROVIDER_TYPE: &str = "gemini_cli";
|
||||
pub const GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME: &str = "gemini_cli:v1internal";
|
||||
|
||||
pub fn is_gemini_cli_provider_transport(transport: &GatewayProviderTransportSnapshot) -> bool {
|
||||
transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(GEMINI_CLI_PROVIDER_TYPE)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::snapshot::GatewayProviderTransportSnapshot;
|
||||
use super::auth::GeminiCliRequestAuth;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum GeminiCliRequestEnvelopeSupport {
|
||||
@@ -12,247 +12,232 @@ pub enum GeminiCliRequestEnvelopeSupport {
|
||||
pub enum GeminiCliRequestEnvelopeUnsupportedReason {
|
||||
NonObjectBody,
|
||||
MissingContents,
|
||||
MissingProjectId,
|
||||
MissingUserPromptId,
|
||||
MissingModel,
|
||||
}
|
||||
|
||||
pub fn resolve_gemini_cli_project_id(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> Option<String> {
|
||||
transport
|
||||
.key
|
||||
.upstream_metadata
|
||||
.as_ref()
|
||||
.and_then(|metadata| {
|
||||
find_string_by_paths(
|
||||
metadata,
|
||||
&[
|
||||
&["gemini_cli", "project_id"],
|
||||
&["gemini_cli", "projectId"],
|
||||
&["gemini_cli", "cloudaicompanionProject"],
|
||||
&["gemini_cli", "cloudaicompanionProject", "id"],
|
||||
&["project_id"],
|
||||
&["projectId"],
|
||||
],
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
transport
|
||||
.key
|
||||
.decrypted_auth_config
|
||||
.as_deref()
|
||||
.and_then(parse_project_id_from_auth_config)
|
||||
})
|
||||
}
|
||||
pub fn classify_gemini_cli_v1internal_request_body(
|
||||
request_body: &Value,
|
||||
) -> Result<(), GeminiCliRequestEnvelopeUnsupportedReason> {
|
||||
let Value::Object(map) = request_body else {
|
||||
return Err(GeminiCliRequestEnvelopeUnsupportedReason::NonObjectBody);
|
||||
};
|
||||
if !map.contains_key("contents") && existing_v1internal_request_object(map).is_none() {
|
||||
return Err(GeminiCliRequestEnvelopeUnsupportedReason::MissingContents);
|
||||
}
|
||||
|
||||
fn parse_project_id_from_auth_config(raw_auth_config: &str) -> Option<String> {
|
||||
let auth_config = serde_json::from_str::<Value>(raw_auth_config).ok()?;
|
||||
find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["project_id"],
|
||||
&["projectId"],
|
||||
&["project", "id"],
|
||||
&["project", "project_id"],
|
||||
&["project", "projectId"],
|
||||
&["gemini_cli", "project_id"],
|
||||
&["gemini_cli", "projectId"],
|
||||
&["metadata", "project_id"],
|
||||
&["metadata", "projectId"],
|
||||
],
|
||||
)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_gemini_cli_v1internal_request(
|
||||
project_id: &str,
|
||||
auth: &GeminiCliRequestAuth,
|
||||
user_prompt_id: &str,
|
||||
model: &str,
|
||||
request_body: &Value,
|
||||
) -> GeminiCliRequestEnvelopeSupport {
|
||||
let project_id = project_id.trim();
|
||||
if project_id.is_empty() {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingProjectId,
|
||||
);
|
||||
}
|
||||
let user_prompt_id = user_prompt_id.trim();
|
||||
if user_prompt_id.is_empty() {
|
||||
if user_prompt_id.trim().is_empty() {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingUserPromptId,
|
||||
);
|
||||
}
|
||||
let model = model.trim();
|
||||
if model.is_empty() {
|
||||
if model.trim().is_empty() {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingModel,
|
||||
);
|
||||
}
|
||||
if let Err(reason) = classify_gemini_cli_v1internal_request_body(request_body) {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(reason);
|
||||
}
|
||||
|
||||
let Value::Object(source) = request_body else {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::NonObjectBody,
|
||||
);
|
||||
};
|
||||
if !source.contains_key("contents") {
|
||||
return GeminiCliRequestEnvelopeSupport::Unsupported(
|
||||
GeminiCliRequestEnvelopeUnsupportedReason::MissingContents,
|
||||
);
|
||||
|
||||
let existing_request = existing_v1internal_request_object(source);
|
||||
let mut inner_request: Map<String, Value> =
|
||||
existing_request.cloned().unwrap_or_else(|| source.clone());
|
||||
sanitize_inner_request(&mut inner_request);
|
||||
maybe_insert_session_id(&mut inner_request, auth.session_id.as_deref());
|
||||
|
||||
let project = non_empty_string_field(source, "project")
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
auth.project_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
});
|
||||
let user_prompt_id = non_empty_string_field(source, "user_prompt_id")
|
||||
.or_else(|| non_empty_string_field(source, "userPromptId"))
|
||||
.unwrap_or(user_prompt_id)
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let mut envelope = Map::new();
|
||||
envelope.insert("model".to_string(), Value::String(model.trim().to_string()));
|
||||
if let Some(project) = project {
|
||||
envelope.insert("project".to_string(), Value::String(project));
|
||||
}
|
||||
envelope.insert("user_prompt_id".to_string(), Value::String(user_prompt_id));
|
||||
envelope.insert("request".to_string(), Value::Object(inner_request));
|
||||
|
||||
let mut inner_request: Map<String, Value> = source.clone();
|
||||
inner_request.remove("model");
|
||||
inner_request.remove("stream");
|
||||
|
||||
GeminiCliRequestEnvelopeSupport::Supported(serde_json::json!({
|
||||
"model": model,
|
||||
"project": project_id,
|
||||
"user_prompt_id": user_prompt_id,
|
||||
"request": Value::Object(inner_request),
|
||||
}))
|
||||
GeminiCliRequestEnvelopeSupport::Supported(Value::Object(envelope))
|
||||
}
|
||||
|
||||
fn find_string_by_paths(value: &Value, paths: &[&[&str]]) -> Option<String> {
|
||||
for path in paths {
|
||||
let mut current = value;
|
||||
let mut matched = true;
|
||||
for segment in *path {
|
||||
let Some(next) = current.get(*segment) else {
|
||||
matched = false;
|
||||
break;
|
||||
};
|
||||
current = next;
|
||||
}
|
||||
if !matched {
|
||||
continue;
|
||||
}
|
||||
if let Some(string) = current
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
{
|
||||
return Some(string.to_string());
|
||||
}
|
||||
}
|
||||
fn sanitize_inner_request(inner_request: &mut Map<String, Value>) {
|
||||
inner_request.remove("model");
|
||||
inner_request.remove("stream");
|
||||
}
|
||||
|
||||
None
|
||||
fn maybe_insert_session_id(inner_request: &mut Map<String, Value>, session_id: Option<&str>) {
|
||||
let Some(session_id) = session_id.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
if inner_request.contains_key("session_id") || inner_request.contains_key("sessionId") {
|
||||
return;
|
||||
}
|
||||
inner_request.insert(
|
||||
"session_id".to_string(),
|
||||
Value::String(session_id.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn existing_v1internal_request_object(source: &Map<String, Value>) -> Option<&Map<String, Value>> {
|
||||
source
|
||||
.get("request")
|
||||
.and_then(Value::as_object)
|
||||
.filter(|request| request.contains_key("contents"))
|
||||
}
|
||||
|
||||
fn non_empty_string_field<'a>(source: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
|
||||
source
|
||||
.get(key)
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::snapshot::{
|
||||
GatewayProviderTransportEndpoint, GatewayProviderTransportKey,
|
||||
GatewayProviderTransportProvider, GatewayProviderTransportSnapshot,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_gemini_cli_v1internal_request, resolve_gemini_cli_project_id,
|
||||
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
|
||||
GeminiCliRequestEnvelopeSupport,
|
||||
};
|
||||
use crate::gemini_cli::GeminiCliRequestAuth;
|
||||
|
||||
fn sample_transport() -> GatewayProviderTransportSnapshot {
|
||||
GatewayProviderTransportSnapshot {
|
||||
provider: GatewayProviderTransportProvider {
|
||||
id: "provider-1".to_string(),
|
||||
name: "Gemini CLI".to_string(),
|
||||
provider_type: "gemini_cli".to_string(),
|
||||
website: None,
|
||||
is_active: true,
|
||||
keep_priority_on_conversion: false,
|
||||
enable_format_conversion: false,
|
||||
concurrent_limit: None,
|
||||
max_retries: None,
|
||||
proxy: None,
|
||||
request_timeout_secs: None,
|
||||
stream_first_byte_timeout_secs: None,
|
||||
config: None,
|
||||
},
|
||||
endpoint: GatewayProviderTransportEndpoint {
|
||||
id: "endpoint-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
api_format: "gemini:generate_content".to_string(),
|
||||
api_family: None,
|
||||
endpoint_kind: None,
|
||||
is_active: true,
|
||||
base_url: "https://cloudcode-pa.googleapis.com".to_string(),
|
||||
header_rules: None,
|
||||
body_rules: None,
|
||||
max_retries: None,
|
||||
custom_path: None,
|
||||
config: None,
|
||||
format_acceptance_config: None,
|
||||
proxy: None,
|
||||
},
|
||||
key: GatewayProviderTransportKey {
|
||||
id: "key-1".to_string(),
|
||||
provider_id: "provider-1".to_string(),
|
||||
name: "key".to_string(),
|
||||
auth_type: "oauth".to_string(),
|
||||
is_active: true,
|
||||
api_formats: None,
|
||||
auth_type_by_format: None,
|
||||
allow_auth_channel_mismatch_formats: None,
|
||||
allowed_models: None,
|
||||
capabilities: None,
|
||||
rate_multipliers: None,
|
||||
global_priority_by_format: None,
|
||||
expires_at_unix_secs: None,
|
||||
proxy: None,
|
||||
fingerprint: None,
|
||||
upstream_metadata: Some(json!({
|
||||
"gemini_cli": {
|
||||
"project_id": "metadata-project"
|
||||
}
|
||||
})),
|
||||
decrypted_api_key: String::new(),
|
||||
decrypted_auth_config: Some(r#"{"project_id":"auth-project"}"#.to_string()),
|
||||
},
|
||||
fn sample_auth() -> GeminiCliRequestAuth {
|
||||
GeminiCliRequestAuth {
|
||||
project_id: Some("project-123".to_string()),
|
||||
session_id: Some("session-123".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_id_prefers_upstream_metadata_then_auth_config() {
|
||||
let mut transport = sample_transport();
|
||||
assert_eq!(
|
||||
resolve_gemini_cli_project_id(&transport).as_deref(),
|
||||
Some("metadata-project")
|
||||
);
|
||||
fn wraps_generate_content_body_in_gemini_cli_v1internal_envelope() {
|
||||
let request_body = json!({
|
||||
"model": "client-model",
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"stream": true,
|
||||
"generationConfig": {"temperature": 0.2}
|
||||
});
|
||||
|
||||
transport.key.upstream_metadata = None;
|
||||
assert_eq!(
|
||||
resolve_gemini_cli_project_id(&transport).as_deref(),
|
||||
Some("auth-project")
|
||||
classify_gemini_cli_v1internal_request_body(&request_body),
|
||||
Ok(())
|
||||
);
|
||||
assert_eq!(
|
||||
build_gemini_cli_v1internal_request(
|
||||
&sample_auth(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"project": "project-123",
|
||||
"user_prompt_id": "trace-123",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"generationConfig": {"temperature": 0.2},
|
||||
"session_id": "session-123"
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wraps_gemini_body_in_code_assist_envelope() {
|
||||
let body = json!({
|
||||
"model": "ignored",
|
||||
"stream": true,
|
||||
"contents": [{"role": "user", "parts": [{"text": "hi"}]}],
|
||||
"generationConfig": {"temperature": 0.2}
|
||||
fn preserves_existing_v1internal_request_shape_without_antigravity_fields() {
|
||||
let request_body = json!({
|
||||
"model": "old-model",
|
||||
"project": "project-from-body",
|
||||
"user_prompt_id": "prompt-from-body",
|
||||
"request": {
|
||||
"model": "nested-client-model",
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"stream": false,
|
||||
"labels": {"source": "test"}
|
||||
},
|
||||
"userAgent": "antigravity",
|
||||
"requestType": "agent"
|
||||
});
|
||||
|
||||
let envelope = match build_gemini_cli_v1internal_request(
|
||||
"project-1",
|
||||
"trace-1",
|
||||
"gemini-2.5-pro",
|
||||
&body,
|
||||
) {
|
||||
GeminiCliRequestEnvelopeSupport::Supported(value) => value,
|
||||
other => panic!("expected supported envelope, got {other:?}"),
|
||||
};
|
||||
|
||||
assert_eq!(envelope["project"], json!("project-1"));
|
||||
assert_eq!(envelope["user_prompt_id"], json!("trace-1"));
|
||||
assert_eq!(envelope["model"], json!("gemini-2.5-pro"));
|
||||
assert_eq!(
|
||||
envelope["request"]["generationConfig"]["temperature"],
|
||||
json!(0.2)
|
||||
build_gemini_cli_v1internal_request(
|
||||
&sample_auth(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"project": "project-from-body",
|
||||
"user_prompt_id": "prompt-from-body",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
],
|
||||
"labels": {"source": "test"},
|
||||
"session_id": "session-123"
|
||||
}
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omits_optional_project_and_session_when_metadata_is_absent() {
|
||||
let request_body = json!({
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
]
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
build_gemini_cli_v1internal_request(
|
||||
&GeminiCliRequestAuth::default(),
|
||||
"trace-123",
|
||||
"gemini-2.5-pro",
|
||||
&request_body,
|
||||
),
|
||||
GeminiCliRequestEnvelopeSupport::Supported(json!({
|
||||
"model": "gemini-2.5-pro",
|
||||
"user_prompt_id": "trace-123",
|
||||
"request": {
|
||||
"contents": [
|
||||
{"role": "user", "parts": [{"text": "hello"}]}
|
||||
]
|
||||
}
|
||||
}))
|
||||
);
|
||||
assert!(envelope["request"].get("model").is_none());
|
||||
assert!(envelope["request"].get("stream").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ pub use diagnostics::{
|
||||
};
|
||||
pub use gemini_cli::{
|
||||
build_gemini_cli_v1internal_request, build_gemini_cli_v1internal_url,
|
||||
is_gemini_cli_provider_transport, resolve_gemini_cli_project_id,
|
||||
classify_gemini_cli_v1internal_request_body, is_gemini_cli_provider_transport,
|
||||
resolve_gemini_cli_project_id, resolve_local_gemini_cli_request_auth, GeminiCliRequestAuth,
|
||||
GeminiCliRequestAuthSupport, GeminiCliRequestAuthUnsupportedReason,
|
||||
GeminiCliRequestEnvelopeSupport, GeminiCliRequestEnvelopeUnsupportedReason,
|
||||
GeminiCliRequestUrlAction, GEMINI_CLI_PROVIDER_TYPE, GEMINI_CLI_RETRIEVE_USER_QUOTA_PATH,
|
||||
GEMINI_CLI_USER_AGENT, GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
|
||||
|
||||
@@ -290,6 +290,22 @@ fn build_transport_hook_url(
|
||||
aether_ai_formats::normalize_api_format_alias(params.provider_api_format);
|
||||
match normalized_provider_api_format.as_str() {
|
||||
"gemini:generate_content" => {
|
||||
if is_gemini_cli_provider_transport(transport) {
|
||||
let query = params.request_query.map(|raw| {
|
||||
form_urlencoded::parse(raw.as_bytes())
|
||||
.into_owned()
|
||||
.collect::<BTreeMap<String, String>>()
|
||||
});
|
||||
return build_gemini_cli_v1internal_url(
|
||||
&transport.endpoint.base_url,
|
||||
if params.upstream_is_stream {
|
||||
GeminiCliRequestUrlAction::StreamGenerateContent
|
||||
} else {
|
||||
GeminiCliRequestUrlAction::GenerateContent
|
||||
},
|
||||
query.as_ref(),
|
||||
);
|
||||
}
|
||||
if let Some(auth) = resolve_local_vertex_api_key_query_auth(transport) {
|
||||
return build_vertex_api_key_gemini_content_url(
|
||||
params.mapped_model?,
|
||||
@@ -717,6 +733,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_cli_generate_content_uses_v1internal_code_assist_url() {
|
||||
let transport = sample_transport(
|
||||
"gemini_cli",
|
||||
"gemini:generate_content",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: false,
|
||||
request_query: Some("key=blocked&beta=true&foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some("https://cloudcode-pa.googleapis.com/v1internal:generateContent?foo=bar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_cli_stream_generate_content_uses_v1internal_code_assist_url() {
|
||||
let transport = sample_transport(
|
||||
"gemini_cli",
|
||||
"gemini:generate_content",
|
||||
"https://cloudcode-pa.googleapis.com",
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_transport_request_url(
|
||||
&transport,
|
||||
TransportRequestUrlParams {
|
||||
provider_api_format: "gemini:generate_content",
|
||||
mapped_model: Some("gemini-2.5-pro"),
|
||||
upstream_is_stream: true,
|
||||
request_query: Some("foo=bar"),
|
||||
kiro_api_region: None,
|
||||
},
|
||||
)
|
||||
.as_deref(),
|
||||
Some(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse&foo=bar"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertex_gemini_embedding_batch_request_uses_vertex_predict_endpoint() {
|
||||
let mut transport = sample_transport(
|
||||
|
||||
@@ -128,6 +128,12 @@ pub fn classify_same_format_provider_request_behavior(
|
||||
);
|
||||
let report_kind = if is_kiro && !params.require_streaming {
|
||||
"claude_cli_sync_finalize"
|
||||
} else if is_gemini_cli && !params.require_streaming {
|
||||
match params.report_kind {
|
||||
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
|
||||
"gemini_cli_sync_success" => "gemini_cli_sync_finalize",
|
||||
_ => params.report_kind,
|
||||
}
|
||||
} else if is_antigravity && !params.require_streaming {
|
||||
match params.report_kind {
|
||||
"gemini_chat_sync_success" => "gemini_chat_sync_finalize",
|
||||
@@ -408,6 +414,7 @@ pub fn same_format_provider_transport_unsupported_reason_for_trace(
|
||||
);
|
||||
if !behavior.is_antigravity
|
||||
&& !behavior.is_claude_code
|
||||
&& !behavior.is_gemini_cli
|
||||
&& !behavior.is_vertex
|
||||
&& !behavior.is_kiro
|
||||
{
|
||||
@@ -558,6 +565,19 @@ mod tests {
|
||||
assert!(behavior.is_antigravity);
|
||||
assert!(behavior.upstream_is_stream);
|
||||
assert_eq!(behavior.report_kind, "gemini_chat_sync_finalize");
|
||||
|
||||
let gemini_cli = sample_transport("gemini_cli");
|
||||
let behavior = classify_same_format_provider_request_behavior(
|
||||
&gemini_cli,
|
||||
SameFormatProviderRequestBehaviorParams {
|
||||
require_streaming: false,
|
||||
provider_api_format: "gemini:generate_content",
|
||||
report_kind: "gemini_cli_sync_success",
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!behavior.upstream_is_stream);
|
||||
assert_eq!(behavior.report_kind, "gemini_cli_sync_finalize");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user