refactor(workspace): enforce layered crate boundaries

This commit is contained in:
elky
2026-07-15 23:47:19 +08:00
parent a728c090a9
commit 8616fe6ee2
969 changed files with 40187 additions and 27240 deletions
@@ -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,
})
);
}
}
@@ -0,0 +1,22 @@
mod auth;
mod policy;
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 policy::gemini_cli_v1internal_requires_upstream_streaming;
pub use request::{
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
GeminiCliRequestEnvelopeSupport, GeminiCliRequestEnvelopeUnsupportedReason,
};
pub use url::{
build_gemini_cli_v1internal_url, GeminiCliRequestUrlAction,
GEMINI_CLI_RETRIEVE_USER_QUOTA_PATH, GEMINI_CLI_USER_AGENT,
GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE,
};
pub const GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME: &str = "gemini_cli:v1internal";
@@ -0,0 +1,29 @@
pub fn gemini_cli_v1internal_requires_upstream_streaming(
provider_api_format: &str,
client_requires_streaming: bool,
) -> bool {
client_requires_streaming
&& aether_ai_formats::normalize_api_format_alias(provider_api_format)
== "gemini:generate_content"
}
#[cfg(test)]
mod tests {
use super::gemini_cli_v1internal_requires_upstream_streaming;
#[test]
fn v1internal_generate_content_requires_upstream_streaming_for_stream_clients() {
assert!(gemini_cli_v1internal_requires_upstream_streaming(
"gemini:generate_content",
true,
));
assert!(!gemini_cli_v1internal_requires_upstream_streaming(
"gemini:generate_content",
false,
));
assert!(!gemini_cli_v1internal_requires_upstream_streaming(
"openai:chat",
true,
));
}
}
@@ -0,0 +1,243 @@
use serde_json::{Map, Value};
use super::auth::GeminiCliRequestAuth;
#[derive(Debug, Clone, PartialEq)]
pub enum GeminiCliRequestEnvelopeSupport {
Supported(Value),
Unsupported(GeminiCliRequestEnvelopeUnsupportedReason),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GeminiCliRequestEnvelopeUnsupportedReason {
NonObjectBody,
MissingContents,
MissingUserPromptId,
MissingModel,
}
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);
}
Ok(())
}
pub fn build_gemini_cli_v1internal_request(
auth: &GeminiCliRequestAuth,
user_prompt_id: &str,
model: &str,
request_body: &Value,
) -> GeminiCliRequestEnvelopeSupport {
if user_prompt_id.trim().is_empty() {
return GeminiCliRequestEnvelopeSupport::Unsupported(
GeminiCliRequestEnvelopeUnsupportedReason::MissingUserPromptId,
);
}
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,
);
};
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));
GeminiCliRequestEnvelopeSupport::Supported(Value::Object(envelope))
}
fn sanitize_inner_request(inner_request: &mut Map<String, Value>) {
inner_request.remove("model");
inner_request.remove("stream");
}
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 serde_json::json;
use super::{
build_gemini_cli_v1internal_request, classify_gemini_cli_v1internal_request_body,
GeminiCliRequestEnvelopeSupport,
};
use crate::gemini_cli::GeminiCliRequestAuth;
fn sample_auth() -> GeminiCliRequestAuth {
GeminiCliRequestAuth {
project_id: Some("project-123".to_string()),
session_id: Some("session-123".to_string()),
}
}
#[test]
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}
});
assert_eq!(
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 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"
});
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-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"}]}
]
}
}))
);
}
}
@@ -0,0 +1,120 @@
use std::collections::BTreeMap;
use url::form_urlencoded;
pub const GEMINI_CLI_USER_AGENT: &str = "GeminiCLI/0.1.5 (Windows; AMD64)";
pub const GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE: &str = "/v1internal:{action}";
pub const GEMINI_CLI_RETRIEVE_USER_QUOTA_PATH: &str = "/v1internal:retrieveUserQuota";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeminiCliRequestUrlAction {
GenerateContent,
StreamGenerateContent,
}
impl GeminiCliRequestUrlAction {
fn as_str(self) -> &'static str {
match self {
Self::GenerateContent => "generateContent",
Self::StreamGenerateContent => "streamGenerateContent",
}
}
fn is_stream(self) -> bool {
matches!(self, Self::StreamGenerateContent)
}
}
pub fn build_gemini_cli_v1internal_url(
base_url: &str,
action: GeminiCliRequestUrlAction,
query: Option<&BTreeMap<String, String>>,
) -> Option<String> {
let trimmed_base = base_url.trim();
if trimmed_base.is_empty() {
return None;
}
let path = GEMINI_CLI_V1INTERNAL_PATH_TEMPLATE.replace("{action}", action.as_str());
let mut url = format!("{}{}", trimmed_base.trim_end_matches('/'), path);
let mut params = BTreeMap::new();
if let Some(query) = query {
for (key, value) in query {
let key = key.trim();
let value = value.trim();
if key.is_empty()
|| value.is_empty()
|| key.eq_ignore_ascii_case("beta")
|| key.eq_ignore_ascii_case("key")
{
continue;
}
params.insert(key.to_string(), value.to_string());
}
}
if action.is_stream() {
params
.entry(String::from("alt"))
.or_insert_with(|| String::from("sse"));
}
if !params.is_empty() {
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (key, value) in params {
serializer.append_pair(key.as_str(), value.as_str());
}
let query_string = serializer.finish();
if !query_string.is_empty() {
url.push('?');
url.push_str(&query_string);
}
}
Some(url)
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::{
build_gemini_cli_v1internal_url, GeminiCliRequestUrlAction,
GEMINI_CLI_RETRIEVE_USER_QUOTA_PATH,
};
#[test]
fn builds_gemini_cli_stream_url_with_alt_sse() {
let query = BTreeMap::from([
("foo".to_string(), "bar".to_string()),
("key".to_string(), "blocked".to_string()),
]);
assert_eq!(
build_gemini_cli_v1internal_url(
"https://cloudcode-pa.googleapis.com/",
GeminiCliRequestUrlAction::StreamGenerateContent,
Some(&query),
)
.as_deref(),
Some("https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse&foo=bar")
);
}
#[test]
fn builds_gemini_cli_sync_url_without_stream_query() {
assert_eq!(
build_gemini_cli_v1internal_url(
"https://cloudcode-pa.googleapis.com",
GeminiCliRequestUrlAction::GenerateContent,
None,
)
.as_deref(),
Some("https://cloudcode-pa.googleapis.com/v1internal:generateContent")
);
assert_eq!(
GEMINI_CLI_RETRIEVE_USER_QUOTA_PATH,
"/v1internal:retrieveUserQuota"
);
}
}