mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
refactor: 大规模模块拆分与代码精简,新增 ai-pipeline/data-contracts 独立 crate
- 新增 aether-ai-pipeline 和 aether-data-contracts crate,将 pipeline 逻辑与数据契约从 gateway 中解耦 - 重构 admin handlers:拆分单体模块为 auth/billing/endpoint/features/model/observability/provider/system 等独立子模块 - 合并 chat/cli 重复代码路径:精简 conversion、finalize、planner 中的 sync/chat/cli 分支 - 重构 scheduler/executor/data 层,引入 facade 模式降低模块间耦合 - 移除冗余的 intent 模块,将 plan_fallback/policy/stream_path/sync_path 迁移至 executor - 前端适配:调整 admin API 调用和 provider 模型测试对话框
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::Response;
|
||||
|
||||
mod models;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_provider_query_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_provider_query_response(state, request_context, request_body)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use super::shared::{
|
||||
build_admin_provider_query_bad_request_response, build_admin_provider_query_not_found_response,
|
||||
provider_query_extract_api_key_id, provider_query_extract_provider_id,
|
||||
ADMIN_PROVIDER_QUERY_API_KEY_NOT_FOUND_DETAIL, ADMIN_PROVIDER_QUERY_NO_ACTIVE_API_KEY_DETAIL,
|
||||
ADMIN_PROVIDER_QUERY_NO_LOCAL_MODELS_DETAIL, ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_NOT_FOUND_DETAIL,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
use axum::{body::Body, http::Response, response::IntoResponse, Json};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_MESSAGE: &str =
|
||||
"Rust local provider-query model test is not configured";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_FAILOVER_MESSAGE: &str =
|
||||
"Rust local provider-query failover simulation is not configured";
|
||||
|
||||
fn provider_query_string_list(value: Option<&serde_json::Value>) -> Vec<String> {
|
||||
value
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|item| !item.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn provider_query_resolved_api_formats(
|
||||
endpoints: &[StoredProviderCatalogEndpoint],
|
||||
selected_key: Option<&StoredProviderCatalogKey>,
|
||||
) -> Vec<String> {
|
||||
let mut seen = BTreeSet::new();
|
||||
let key_formats = selected_key
|
||||
.map(|key| provider_query_string_list(key.api_formats.as_ref()))
|
||||
.unwrap_or_default();
|
||||
let mut formats = Vec::new();
|
||||
|
||||
for endpoint in endpoints.iter().filter(|endpoint| endpoint.is_active) {
|
||||
let api_format = endpoint.api_format.trim();
|
||||
if api_format.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !key_formats.is_empty() && !key_formats.iter().any(|value| value == api_format) {
|
||||
continue;
|
||||
}
|
||||
if seen.insert(api_format.to_string()) {
|
||||
formats.push(api_format.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if formats.is_empty() {
|
||||
for api_format in key_formats {
|
||||
if seen.insert(api_format.clone()) {
|
||||
formats.push(api_format);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formats
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_provider_query_models_response(
|
||||
state: &AppState,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(provider_id) = provider_query_extract_provider_id(payload) else {
|
||||
return Ok(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
));
|
||||
};
|
||||
|
||||
let Some(provider) = state
|
||||
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|item| item.id == provider_id)
|
||||
else {
|
||||
return Ok(build_admin_provider_query_not_found_response(
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_NOT_FOUND_DETAIL,
|
||||
));
|
||||
};
|
||||
|
||||
let provider_ids = vec![provider.id.clone()];
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
let keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?;
|
||||
let selected_key = if let Some(api_key_id) = provider_query_extract_api_key_id(payload) {
|
||||
let Some(key) = keys.iter().find(|key| key.id == api_key_id) else {
|
||||
return Ok(build_admin_provider_query_not_found_response(
|
||||
ADMIN_PROVIDER_QUERY_API_KEY_NOT_FOUND_DETAIL,
|
||||
));
|
||||
};
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let active_keys = keys.iter().filter(|key| key.is_active).count();
|
||||
if selected_key.is_none() && active_keys == 0 {
|
||||
return Ok(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_NO_ACTIVE_API_KEY_DETAIL,
|
||||
));
|
||||
}
|
||||
|
||||
let resolved_api_formats = provider_query_resolved_api_formats(&endpoints, selected_key);
|
||||
let provider_models = state
|
||||
.list_admin_provider_available_source_models(&provider.id)
|
||||
.await?;
|
||||
|
||||
let mut grouped: BTreeMap<
|
||||
String,
|
||||
(
|
||||
aether_data_contracts::repository::global_models::StoredAdminProviderModel,
|
||||
BTreeSet<String>,
|
||||
),
|
||||
> = BTreeMap::new();
|
||||
for model in provider_models {
|
||||
let entry = grouped
|
||||
.entry(model.provider_model_name.clone())
|
||||
.or_insert_with(|| (model.clone(), BTreeSet::new()));
|
||||
for api_format in &resolved_api_formats {
|
||||
entry.1.insert(api_format.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let models: Vec<_> = grouped
|
||||
.into_iter()
|
||||
.map(|(model_id, (model, api_formats))| {
|
||||
let display_name = model
|
||||
.global_model_display_name
|
||||
.clone()
|
||||
.or(model.global_model_name.clone())
|
||||
.unwrap_or_else(|| model_id.clone());
|
||||
let api_formats: Vec<_> = api_formats.into_iter().collect();
|
||||
json!({
|
||||
"id": model_id,
|
||||
"object": "model",
|
||||
"created": model.created_at_unix_secs,
|
||||
"owned_by": provider.name,
|
||||
"display_name": display_name,
|
||||
"api_format": api_formats.first().cloned(),
|
||||
"api_formats": api_formats,
|
||||
"provider_model_name": model.provider_model_name,
|
||||
"global_model_id": model.global_model_id,
|
||||
"global_model_name": model.global_model_name,
|
||||
"supports_streaming": model.supports_streaming,
|
||||
"supports_function_calling": model.supports_function_calling,
|
||||
"supports_vision": model.supports_vision,
|
||||
"supports_extended_thinking": model.supports_extended_thinking,
|
||||
"supports_image_generation": model.supports_image_generation,
|
||||
"is_available": model.is_available,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let success = !models.is_empty();
|
||||
let error = if success {
|
||||
None
|
||||
} else {
|
||||
Some(ADMIN_PROVIDER_QUERY_NO_LOCAL_MODELS_DETAIL)
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"success": success,
|
||||
"data": {
|
||||
"models": models,
|
||||
"error": error,
|
||||
"from_cache": true,
|
||||
"keys_total": active_keys,
|
||||
"keys_cached": 0,
|
||||
"keys_fetched": 0,
|
||||
},
|
||||
"provider": {
|
||||
"id": provider.id,
|
||||
"name": provider.name,
|
||||
"display_name": provider.name,
|
||||
},
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_provider_query_test_model_response(
|
||||
provider_id: String,
|
||||
model: String,
|
||||
) -> Response<Body> {
|
||||
Json(json!({
|
||||
"success": false,
|
||||
"tested": false,
|
||||
"provider_id": provider_id,
|
||||
"model": model,
|
||||
"attempts": [],
|
||||
"total_candidates": 0,
|
||||
"total_attempts": 0,
|
||||
"error": ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_MESSAGE,
|
||||
"source": "local",
|
||||
"message": ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_MESSAGE,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_provider_query_test_model_failover_response(
|
||||
provider_id: String,
|
||||
failover_models: Vec<String>,
|
||||
) -> Response<Body> {
|
||||
Json(json!({
|
||||
"success": false,
|
||||
"tested": false,
|
||||
"provider_id": provider_id,
|
||||
"model": failover_models.first().cloned(),
|
||||
"failover_models": failover_models,
|
||||
"attempts": [],
|
||||
"total_candidates": 0,
|
||||
"total_attempts": 0,
|
||||
"error": ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_FAILOVER_MESSAGE,
|
||||
"source": "local",
|
||||
"message": ADMIN_PROVIDER_QUERY_LOCAL_TEST_MODEL_FAILOVER_MESSAGE,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use super::models::{
|
||||
build_admin_provider_query_models_response,
|
||||
build_admin_provider_query_test_model_failover_response,
|
||||
build_admin_provider_query_test_model_response,
|
||||
};
|
||||
use super::shared::{
|
||||
build_admin_provider_query_bad_request_response, parse_admin_provider_query_body,
|
||||
provider_query_extract_failover_models, provider_query_extract_model,
|
||||
provider_query_extract_provider_id, provider_query_extract_request_id,
|
||||
provider_query_payload_keys, ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
|
||||
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL, ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::log_ids::short_request_id;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
http::Response,
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
fn log_admin_provider_query_validation_failure(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
route_kind: &str,
|
||||
detail: &'static str,
|
||||
payload: &serde_json::Value,
|
||||
) {
|
||||
let provider_id =
|
||||
provider_query_extract_provider_id(payload).unwrap_or_else(|| "-".to_string());
|
||||
let model = provider_query_extract_model(payload).unwrap_or_else(|| "-".to_string());
|
||||
let request_id = provider_query_extract_request_id(payload).unwrap_or_else(|| "-".to_string());
|
||||
let request_id_for_log = short_request_id(request_id.as_str());
|
||||
let payload_keys = provider_query_payload_keys(payload);
|
||||
|
||||
warn!(
|
||||
event_name = "admin_provider_query_request_rejected",
|
||||
log_type = "validation",
|
||||
route_kind,
|
||||
path = %request_context.request_path,
|
||||
request_id = %request_id_for_log,
|
||||
provider_id = %provider_id,
|
||||
model = %model,
|
||||
payload_keys = ?payload_keys,
|
||||
detail,
|
||||
"admin provider query request rejected"
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_provider_query_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if decision.route_family.as_deref() != Some("provider_query_manage") {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if request_context.request_method != http::Method::POST {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let payload = match parse_admin_provider_query_body(request_body) {
|
||||
Ok(value) => value,
|
||||
Err(response) => return Ok(Some(response)),
|
||||
};
|
||||
|
||||
let route_kind = decision.route_kind.as_deref().unwrap_or("query_models");
|
||||
match route_kind {
|
||||
"query_models" => Ok(Some(
|
||||
build_admin_provider_query_models_response(state, &payload).await?,
|
||||
)),
|
||||
"test_model" => {
|
||||
let Some(provider_id) = provider_query_extract_provider_id(&payload) else {
|
||||
log_admin_provider_query_validation_failure(
|
||||
request_context,
|
||||
route_kind,
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
&payload,
|
||||
);
|
||||
return Ok(Some(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
)));
|
||||
};
|
||||
let Some(model) = provider_query_extract_model(&payload) else {
|
||||
log_admin_provider_query_validation_failure(
|
||||
request_context,
|
||||
route_kind,
|
||||
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL,
|
||||
&payload,
|
||||
);
|
||||
return Ok(Some(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL,
|
||||
)));
|
||||
};
|
||||
Ok(Some(build_admin_provider_query_test_model_response(
|
||||
provider_id,
|
||||
model,
|
||||
)))
|
||||
}
|
||||
"test_model_failover" => {
|
||||
let Some(provider_id) = provider_query_extract_provider_id(&payload) else {
|
||||
log_admin_provider_query_validation_failure(
|
||||
request_context,
|
||||
route_kind,
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
&payload,
|
||||
);
|
||||
return Ok(Some(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL,
|
||||
)));
|
||||
};
|
||||
let failover_models = provider_query_extract_failover_models(&payload);
|
||||
if failover_models.is_empty() {
|
||||
log_admin_provider_query_validation_failure(
|
||||
request_context,
|
||||
route_kind,
|
||||
ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
|
||||
&payload,
|
||||
);
|
||||
return Ok(Some(build_admin_provider_query_bad_request_response(
|
||||
ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL,
|
||||
)));
|
||||
}
|
||||
Ok(Some(
|
||||
build_admin_provider_query_test_model_failover_response(
|
||||
provider_id,
|
||||
failover_models,
|
||||
),
|
||||
))
|
||||
}
|
||||
_ => Ok(Some(
|
||||
build_admin_provider_query_models_response(state, &payload).await?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use axum::{
|
||||
body::{Body, Bytes},
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_INVALID_JSON_DETAIL: &str = "Invalid JSON request body";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_PROVIDER_ID_REQUIRED_DETAIL: &str = "provider_id is required";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_MODEL_REQUIRED_DETAIL: &str = "model is required";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_FAILOVER_MODELS_REQUIRED_DETAIL: &str =
|
||||
"failover_models should not be empty";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_PROVIDER_NOT_FOUND_DETAIL: &str = "Provider not found";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_API_KEY_NOT_FOUND_DETAIL: &str = "API Key not found";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_NO_ACTIVE_API_KEY_DETAIL: &str =
|
||||
"No active API Key found for this provider";
|
||||
pub(super) const ADMIN_PROVIDER_QUERY_NO_LOCAL_MODELS_DETAIL: &str =
|
||||
"No models available from local provider catalog";
|
||||
|
||||
pub(super) fn build_admin_provider_query_bad_request_response(
|
||||
detail: &'static str,
|
||||
) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn build_admin_provider_query_not_found_response(
|
||||
detail: &'static str,
|
||||
) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn parse_admin_provider_query_body(
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<serde_json::Value, Response<Body>> {
|
||||
let Some(raw_body) = request_body else {
|
||||
return Ok(json!({}));
|
||||
};
|
||||
if raw_body.is_empty() {
|
||||
return Ok(json!({}));
|
||||
}
|
||||
serde_json::from_slice::<serde_json::Value>(raw_body).map_err(|_| {
|
||||
build_admin_provider_query_bad_request_response(ADMIN_PROVIDER_QUERY_INVALID_JSON_DETAIL)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_extract_provider_id(payload: &serde_json::Value) -> Option<String> {
|
||||
payload
|
||||
.get("provider_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_extract_api_key_id(payload: &serde_json::Value) -> Option<String> {
|
||||
payload
|
||||
.get("api_key_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_extract_model(payload: &serde_json::Value) -> Option<String> {
|
||||
payload
|
||||
.get("model")
|
||||
.or_else(|| payload.get("model_name"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_extract_failover_models(payload: &serde_json::Value) -> Vec<String> {
|
||||
if let Some(items) = payload
|
||||
.get("failover_models")
|
||||
.or_else(|| payload.get("models"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
{
|
||||
return items
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
provider_query_extract_model(payload)
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_extract_request_id(payload: &serde_json::Value) -> Option<String> {
|
||||
payload
|
||||
.get("request_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub(super) fn provider_query_payload_keys(payload: &serde_json::Value) -> Vec<String> {
|
||||
let Some(object) = payload.as_object() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut keys = object.keys().cloned().collect::<Vec<_>>();
|
||||
keys.sort();
|
||||
keys
|
||||
}
|
||||
Reference in New Issue
Block a user