mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
213
crates/aether-provider-transport/src/antigravity/auth.rs
Normal file
213
crates/aether-provider-transport/src/antigravity/auth.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
|
||||
pub const ANTIGRAVITY_PROVIDER_TYPE: &str = "antigravity";
|
||||
pub const ANTIGRAVITY_REQUEST_USER_AGENT: &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";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AntigravityRequestAuth {
|
||||
pub project_id: String,
|
||||
pub client_version: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestAuthSupport {
|
||||
Supported(AntigravityRequestAuth),
|
||||
Unsupported(AntigravityRequestAuthUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestAuthUnsupportedReason {
|
||||
WrongProviderType,
|
||||
MissingAuthConfig,
|
||||
InvalidAuthConfigJson,
|
||||
ComplexDynamicAuthConfig,
|
||||
MissingProjectId,
|
||||
}
|
||||
|
||||
pub fn resolve_local_antigravity_request_auth(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
) -> AntigravityRequestAuthSupport {
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(ANTIGRAVITY_PROVIDER_TYPE)
|
||||
{
|
||||
return AntigravityRequestAuthSupport::Unsupported(
|
||||
AntigravityRequestAuthUnsupportedReason::WrongProviderType,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(raw_auth_config) = transport
|
||||
.key
|
||||
.decrypted_auth_config
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return AntigravityRequestAuthSupport::Unsupported(
|
||||
AntigravityRequestAuthUnsupportedReason::MissingAuthConfig,
|
||||
);
|
||||
};
|
||||
|
||||
let Ok(auth_config) = serde_json::from_str::<Value>(raw_auth_config) else {
|
||||
return AntigravityRequestAuthSupport::Unsupported(
|
||||
AntigravityRequestAuthUnsupportedReason::InvalidAuthConfigJson,
|
||||
);
|
||||
};
|
||||
|
||||
if contains_blocked_auth_fields(&auth_config) {
|
||||
return AntigravityRequestAuthSupport::Unsupported(
|
||||
AntigravityRequestAuthUnsupportedReason::ComplexDynamicAuthConfig,
|
||||
);
|
||||
}
|
||||
|
||||
let Some(project_id) = find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["project_id"],
|
||||
&["projectId"],
|
||||
&["project", "id"],
|
||||
&["project", "project_id"],
|
||||
&["project", "projectId"],
|
||||
&["antigravity", "project_id"],
|
||||
&["antigravity", "projectId"],
|
||||
&["metadata", "project_id"],
|
||||
&["metadata", "projectId"],
|
||||
],
|
||||
) else {
|
||||
return AntigravityRequestAuthSupport::Unsupported(
|
||||
AntigravityRequestAuthUnsupportedReason::MissingProjectId,
|
||||
);
|
||||
};
|
||||
|
||||
let client_version = find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["client_version"],
|
||||
&["clientVersion"],
|
||||
&["antigravity", "client_version"],
|
||||
&["antigravity", "clientVersion"],
|
||||
&["metadata", "client_version"],
|
||||
&["metadata", "clientVersion"],
|
||||
],
|
||||
);
|
||||
let session_id = find_string_by_paths(
|
||||
&auth_config,
|
||||
&[
|
||||
&["session_id"],
|
||||
&["sessionId"],
|
||||
&["antigravity", "session_id"],
|
||||
&["antigravity", "sessionId"],
|
||||
&["metadata", "session_id"],
|
||||
&["metadata", "sessionId"],
|
||||
],
|
||||
);
|
||||
|
||||
AntigravityRequestAuthSupport::Supported(AntigravityRequestAuth {
|
||||
project_id,
|
||||
client_version,
|
||||
session_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_antigravity_static_identity_headers(
|
||||
auth: &AntigravityRequestAuth,
|
||||
) -> BTreeMap<String, String> {
|
||||
let mut headers = BTreeMap::from([
|
||||
(
|
||||
String::from("x-client-name"),
|
||||
String::from(ANTIGRAVITY_CLIENT_NAME),
|
||||
),
|
||||
(
|
||||
String::from("x-goog-api-client"),
|
||||
String::from(ANTIGRAVITY_GOOG_API_CLIENT),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(client_version) = auth
|
||||
.client_version
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
headers.insert(String::from("x-client-version"), client_version.to_string());
|
||||
}
|
||||
if let Some(session_id) = auth
|
||||
.session_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
headers.insert(String::from("x-vscode-sessionid"), session_id.to_string());
|
||||
}
|
||||
|
||||
headers
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn contains_blocked_auth_fields(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Object(map) => map.iter().any(|(key, inner)| {
|
||||
is_blocked_auth_key(key.as_str()) || contains_blocked_auth_fields(inner)
|
||||
}),
|
||||
Value::Array(items) => items.iter().any(contains_blocked_auth_fields),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_blocked_auth_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key.trim().to_ascii_lowercase().as_str(),
|
||||
"private_key"
|
||||
| "privateKey"
|
||||
| "private_key_id"
|
||||
| "privateKeyId"
|
||||
| "service_account"
|
||||
| "serviceAccount"
|
||||
| "service_account_json"
|
||||
| "serviceAccountJson"
|
||||
| "service_account_key"
|
||||
| "serviceAccountKey"
|
||||
| "credential_source"
|
||||
| "credentialSource"
|
||||
| "token_url"
|
||||
| "tokenUrl"
|
||||
| "auth_uri"
|
||||
| "authUri"
|
||||
| "subject"
|
||||
| "audience"
|
||||
)
|
||||
}
|
||||
113
crates/aether-provider-transport/src/antigravity/policy.rs
Normal file
113
crates/aether-provider-transport/src/antigravity/policy.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use serde_json::Value;
|
||||
|
||||
use super::super::snapshot::GatewayProviderTransportSnapshot;
|
||||
use super::auth::{
|
||||
resolve_local_antigravity_request_auth, AntigravityRequestAuth, AntigravityRequestAuthSupport,
|
||||
AntigravityRequestAuthUnsupportedReason, ANTIGRAVITY_PROVIDER_TYPE,
|
||||
};
|
||||
use super::request::{
|
||||
classify_antigravity_safe_request_body, AntigravityEnvelopeRequestType,
|
||||
AntigravityRequestEnvelopeUnsupportedReason,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AntigravityRequestSideSpec {
|
||||
pub auth: AntigravityRequestAuth,
|
||||
pub request_type: AntigravityEnvelopeRequestType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestSideSupport {
|
||||
Supported(AntigravityRequestSideSpec),
|
||||
Unsupported(AntigravityRequestSideUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestSideUnsupportedReason {
|
||||
InactiveTransport,
|
||||
WrongProviderType,
|
||||
UnsupportedApiFormat,
|
||||
UnsupportedCustomPath,
|
||||
UnsupportedHeaderRules,
|
||||
UnsupportedBodyRules,
|
||||
UnsupportedNetworkConfig,
|
||||
UnsupportedAuth(AntigravityRequestAuthUnsupportedReason),
|
||||
UnsupportedEnvelope(AntigravityRequestEnvelopeUnsupportedReason),
|
||||
}
|
||||
|
||||
pub fn classify_local_antigravity_request_support(
|
||||
transport: &GatewayProviderTransportSnapshot,
|
||||
request_body: &Value,
|
||||
request_type: AntigravityEnvelopeRequestType,
|
||||
) -> AntigravityRequestSideSupport {
|
||||
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::InactiveTransport,
|
||||
);
|
||||
}
|
||||
if !transport
|
||||
.provider
|
||||
.provider_type
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(ANTIGRAVITY_PROVIDER_TYPE)
|
||||
{
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::WrongProviderType,
|
||||
);
|
||||
}
|
||||
|
||||
let endpoint_format = transport.endpoint.api_format.trim();
|
||||
if !endpoint_format.eq_ignore_ascii_case("gemini:chat")
|
||||
&& !endpoint_format.eq_ignore_ascii_case("gemini:cli")
|
||||
{
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedApiFormat,
|
||||
);
|
||||
}
|
||||
if transport
|
||||
.endpoint
|
||||
.custom_path
|
||||
.as_deref()
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
{
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedCustomPath,
|
||||
);
|
||||
}
|
||||
if transport.endpoint.header_rules.is_some() {
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedHeaderRules,
|
||||
);
|
||||
}
|
||||
if transport.endpoint.body_rules.is_some() {
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedBodyRules,
|
||||
);
|
||||
}
|
||||
if transport.provider.proxy.is_some()
|
||||
|| transport.endpoint.proxy.is_some()
|
||||
|| transport.key.proxy.is_some()
|
||||
|| transport.key.fingerprint.is_some()
|
||||
{
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedNetworkConfig,
|
||||
);
|
||||
}
|
||||
|
||||
let auth = match resolve_local_antigravity_request_auth(transport) {
|
||||
AntigravityRequestAuthSupport::Supported(auth) => auth,
|
||||
AntigravityRequestAuthSupport::Unsupported(reason) => {
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedAuth(reason),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(reason) = classify_antigravity_safe_request_body(request_body) {
|
||||
return AntigravityRequestSideSupport::Unsupported(
|
||||
AntigravityRequestSideUnsupportedReason::UnsupportedEnvelope(reason),
|
||||
);
|
||||
}
|
||||
|
||||
AntigravityRequestSideSupport::Supported(AntigravityRequestSideSpec { auth, request_type })
|
||||
}
|
||||
120
crates/aether-provider-transport/src/antigravity/request.rs
Normal file
120
crates/aether-provider-transport/src/antigravity/request.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::auth::{AntigravityRequestAuth, ANTIGRAVITY_REQUEST_USER_AGENT};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AntigravityEnvelopeRequestType {
|
||||
Agent,
|
||||
EndpointTest,
|
||||
}
|
||||
|
||||
impl AntigravityEnvelopeRequestType {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Agent => "agent",
|
||||
Self::EndpointTest => "endpoint_test",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum AntigravityRequestEnvelopeSupport {
|
||||
Supported(Value),
|
||||
Unsupported(AntigravityRequestEnvelopeUnsupportedReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestEnvelopeUnsupportedReason {
|
||||
NonObjectBody,
|
||||
MissingContents,
|
||||
MissingRequestId,
|
||||
MissingModel,
|
||||
ComplexEnvelopeTransform,
|
||||
}
|
||||
|
||||
pub fn classify_antigravity_safe_request_body(
|
||||
request_body: &Value,
|
||||
) -> Result<(), AntigravityRequestEnvelopeUnsupportedReason> {
|
||||
let Value::Object(map) = request_body else {
|
||||
return Err(AntigravityRequestEnvelopeUnsupportedReason::NonObjectBody);
|
||||
};
|
||||
if !map.contains_key("contents") {
|
||||
return Err(AntigravityRequestEnvelopeUnsupportedReason::MissingContents);
|
||||
}
|
||||
if contains_blocked_request_features(request_body) {
|
||||
return Err(AntigravityRequestEnvelopeUnsupportedReason::ComplexEnvelopeTransform);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build_antigravity_safe_v1internal_request(
|
||||
auth: &AntigravityRequestAuth,
|
||||
request_id: &str,
|
||||
model: &str,
|
||||
request_body: &Value,
|
||||
request_type: AntigravityEnvelopeRequestType,
|
||||
) -> AntigravityRequestEnvelopeSupport {
|
||||
if request_id.trim().is_empty() {
|
||||
return AntigravityRequestEnvelopeSupport::Unsupported(
|
||||
AntigravityRequestEnvelopeUnsupportedReason::MissingRequestId,
|
||||
);
|
||||
}
|
||||
if model.trim().is_empty() {
|
||||
return AntigravityRequestEnvelopeSupport::Unsupported(
|
||||
AntigravityRequestEnvelopeUnsupportedReason::MissingModel,
|
||||
);
|
||||
}
|
||||
if let Err(reason) = classify_antigravity_safe_request_body(request_body) {
|
||||
return AntigravityRequestEnvelopeSupport::Unsupported(reason);
|
||||
}
|
||||
|
||||
let Value::Object(source) = request_body else {
|
||||
return AntigravityRequestEnvelopeSupport::Unsupported(
|
||||
AntigravityRequestEnvelopeUnsupportedReason::NonObjectBody,
|
||||
);
|
||||
};
|
||||
|
||||
let mut inner_request: Map<String, Value> = source.clone();
|
||||
inner_request.remove("model");
|
||||
inner_request.remove("safetySettings");
|
||||
inner_request.remove("safety_settings");
|
||||
|
||||
AntigravityRequestEnvelopeSupport::Supported(serde_json::json!({
|
||||
"project": auth.project_id,
|
||||
"requestId": request_id,
|
||||
"request": Value::Object(inner_request),
|
||||
"model": model,
|
||||
"userAgent": ANTIGRAVITY_REQUEST_USER_AGENT,
|
||||
"requestType": request_type.as_str(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn contains_blocked_request_features(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Object(map) => map.iter().any(|(key, inner)| {
|
||||
is_blocked_request_key(key.as_str()) || contains_blocked_request_features(inner)
|
||||
}),
|
||||
Value::Array(items) => items.iter().any(contains_blocked_request_features),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_blocked_request_key(key: &str) -> bool {
|
||||
matches!(
|
||||
key.trim(),
|
||||
"systemInstruction"
|
||||
| "system_instruction"
|
||||
| "tools"
|
||||
| "toolConfig"
|
||||
| "tool_config"
|
||||
| "thinkingConfig"
|
||||
| "thinking_config"
|
||||
| "imageConfig"
|
||||
| "image_config"
|
||||
| "functionCall"
|
||||
| "function_call"
|
||||
| "functionResponse"
|
||||
| "function_response"
|
||||
)
|
||||
}
|
||||
69
crates/aether-provider-transport/src/antigravity/url.rs
Normal file
69
crates/aether-provider-transport/src/antigravity/url.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use url::form_urlencoded;
|
||||
|
||||
pub const ANTIGRAVITY_V1INTERNAL_PATH_TEMPLATE: &str = "/v1internal:{action}";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AntigravityRequestUrlAction {
|
||||
GenerateContent,
|
||||
StreamGenerateContent,
|
||||
}
|
||||
|
||||
impl AntigravityRequestUrlAction {
|
||||
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_antigravity_v1internal_url(
|
||||
base_url: &str,
|
||||
action: AntigravityRequestUrlAction,
|
||||
query: Option<&BTreeMap<String, String>>,
|
||||
) -> Option<String> {
|
||||
let trimmed_base = base_url.trim();
|
||||
if trimmed_base.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = ANTIGRAVITY_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") {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user