mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(models): add external catalog proxy selection
This commit is contained in:
@@ -84,6 +84,7 @@ pub(crate) async fn maybe_build_local_admin_core_response(
|
||||
crate::handlers::admin::model::maybe_build_local_admin_model_catalog_response(
|
||||
state,
|
||||
request_context,
|
||||
request_body,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
|
||||
@@ -7,5 +7,8 @@ mod routes;
|
||||
pub(super) mod shared;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use self::proxy_nodes::override_proxy_connectivity_probe_url_for_tests;
|
||||
pub(crate) use self::proxy_nodes::{
|
||||
clear_proxy_node_references_with_cache_failure_for_tests,
|
||||
override_proxy_connectivity_probe_url_for_tests,
|
||||
};
|
||||
pub(super) use self::routes::maybe_build_local_admin_system_response;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::future::Future;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::execution_runtime::transport::format_upstream_request_error;
|
||||
use crate::handlers::admin::model::{
|
||||
acquire_admin_external_models_config_mutation_lock,
|
||||
release_admin_external_models_config_mutation_lock,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::maintenance::{
|
||||
@@ -31,6 +36,7 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::warn;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::handlers::public::build_proxy_node_install_session_response;
|
||||
@@ -526,27 +532,43 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
||||
if decision.route_kind.as_deref() == Some("delete_node")
|
||||
&& request_context.method() == http::Method::DELETE
|
||||
{
|
||||
if !state.has_proxy_node_writer() {
|
||||
if !state.has_proxy_node_reader() || !state.has_proxy_node_writer() {
|
||||
return Ok(Some(build_admin_proxy_nodes_data_unavailable_response()));
|
||||
}
|
||||
let Some(node_id) = admin_proxy_node_node_id_from_path(request_context.path()) else {
|
||||
return Ok(Some(build_admin_proxy_nodes_not_found_response()));
|
||||
};
|
||||
let Some(_deleted_node) = state.delete_proxy_node(&node_id).await? else {
|
||||
return Ok(Some(build_admin_proxy_nodes_not_found_response()));
|
||||
let lock = match acquire_admin_external_models_config_mutation_lock(state).await {
|
||||
Ok(lock) => lock,
|
||||
Err((status, payload)) => {
|
||||
return Ok(Some((status, Json(payload)).into_response()));
|
||||
}
|
||||
};
|
||||
let cleanup = clear_deleted_proxy_node_references(state, &node_id).await?;
|
||||
return Ok(Some(
|
||||
Json(json!({
|
||||
let delete_result: Result<Response<Body>, GatewayError> = async {
|
||||
if state.find_proxy_node(&node_id).await?.is_none() {
|
||||
return Ok(build_admin_proxy_nodes_not_found_response());
|
||||
}
|
||||
|
||||
// Persistent references are cleared before the node itself. If a durable cleanup
|
||||
// fails, the node remains available and the operation can be retried safely.
|
||||
let cleanup = clear_proxy_node_references_before_delete(state, &node_id).await?;
|
||||
let Some(_deleted_node) = state.delete_proxy_node(&node_id).await? else {
|
||||
return Ok(build_admin_proxy_nodes_not_found_response());
|
||||
};
|
||||
Ok(Json(json!({
|
||||
"message": build_delete_proxy_node_message(&cleanup),
|
||||
"node_id": node_id,
|
||||
"cleared_system_proxy": cleanup.cleared_system_proxy,
|
||||
"cleared_external_models_proxy": cleanup.cleared_external_models_proxy,
|
||||
"cleared_providers": cleanup.cleared_providers,
|
||||
"cleared_endpoints": cleanup.cleared_endpoints,
|
||||
"cleared_keys": cleanup.cleared_keys,
|
||||
}))
|
||||
.into_response(),
|
||||
));
|
||||
.into_response())
|
||||
}
|
||||
.await;
|
||||
release_admin_external_models_config_mutation_lock(state, &lock).await;
|
||||
return delete_result.map(Some);
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("test_node")
|
||||
@@ -869,6 +891,8 @@ pub(crate) async fn maybe_build_local_admin_proxy_nodes_response(
|
||||
#[derive(Debug, Default)]
|
||||
struct DeletedProxyNodeCleanup {
|
||||
cleared_system_proxy: bool,
|
||||
cleared_external_models_proxy: bool,
|
||||
external_models_cache_clear_succeeded: Option<bool>,
|
||||
cleared_providers: usize,
|
||||
cleared_endpoints: usize,
|
||||
cleared_keys: usize,
|
||||
@@ -895,15 +919,32 @@ struct NormalizedManualProxyEndpoint {
|
||||
node_port: i32,
|
||||
}
|
||||
|
||||
async fn clear_deleted_proxy_node_references(
|
||||
async fn clear_proxy_node_references_before_delete(
|
||||
state: &AdminAppState<'_>,
|
||||
node_id: &str,
|
||||
) -> Result<DeletedProxyNodeCleanup, GatewayError> {
|
||||
let external_models_cache_clear = state.clear_admin_external_models_cache();
|
||||
clear_proxy_node_references_before_delete_with_cache(
|
||||
state,
|
||||
node_id,
|
||||
external_models_cache_clear,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn clear_proxy_node_references_before_delete_with_cache<F>(
|
||||
state: &AdminAppState<'_>,
|
||||
node_id: &str,
|
||||
external_models_cache_clear: F,
|
||||
) -> Result<DeletedProxyNodeCleanup, GatewayError>
|
||||
where
|
||||
F: Future<Output = Result<Value, GatewayError>>,
|
||||
{
|
||||
let mut cleanup = DeletedProxyNodeCleanup::default();
|
||||
|
||||
if state.app().data.has_system_config_store() {
|
||||
let is_system_proxy = state
|
||||
.read_system_config_json_value("system_proxy_node_id")
|
||||
.read_system_config_json_value_strong("system_proxy_node_id")
|
||||
.await?
|
||||
.and_then(|value| value.as_str().map(str::trim).map(ToOwned::to_owned))
|
||||
.is_some_and(|value| value == node_id);
|
||||
@@ -917,6 +958,36 @@ async fn clear_deleted_proxy_node_references(
|
||||
.await?;
|
||||
cleanup.cleared_system_proxy = true;
|
||||
}
|
||||
|
||||
let is_external_models_proxy = state
|
||||
.read_system_config_json_value_strong("external_models_proxy_node_id")
|
||||
.await?
|
||||
.and_then(|value| value.as_str().map(str::trim).map(ToOwned::to_owned))
|
||||
.is_some_and(|value| value == node_id);
|
||||
if is_external_models_proxy {
|
||||
state
|
||||
.upsert_system_config_json_value(
|
||||
"external_models_proxy_node_id",
|
||||
&serde_json::Value::Null,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
cleanup.external_models_cache_clear_succeeded =
|
||||
Some(match external_models_cache_clear.await {
|
||||
Ok(_) => true,
|
||||
Err(_) => {
|
||||
// The selector is already persisted as null, and v2 cache entries carry
|
||||
// their selector. A failed DEL therefore cannot route through this node.
|
||||
warn!(
|
||||
runtime_backend = state.app().runtime_state_backend(),
|
||||
proxy_node_id = %node_id,
|
||||
"failed to clear external models cache while deleting proxy node"
|
||||
);
|
||||
false
|
||||
}
|
||||
});
|
||||
cleanup.cleared_external_models_proxy = true;
|
||||
}
|
||||
}
|
||||
|
||||
if state.app().has_provider_catalog_data_reader()
|
||||
@@ -978,11 +1049,39 @@ async fn clear_deleted_proxy_node_references(
|
||||
Ok(cleanup)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn clear_proxy_node_references_with_cache_failure_for_tests(
|
||||
app: &crate::AppState,
|
||||
node_id: &str,
|
||||
) -> Result<Value, GatewayError> {
|
||||
let state = AdminAppState::new(app);
|
||||
let cleanup = clear_proxy_node_references_before_delete_with_cache(&state, node_id, async {
|
||||
Err(GatewayError::Internal(
|
||||
"injected cache delete failure".to_string(),
|
||||
))
|
||||
})
|
||||
.await?;
|
||||
Ok(json!({
|
||||
"cleared_system_proxy": cleanup.cleared_system_proxy,
|
||||
"cleared_external_models_proxy": cleanup.cleared_external_models_proxy,
|
||||
"external_models_cache_clear_succeeded": cleanup.external_models_cache_clear_succeeded,
|
||||
"cleared_providers": cleanup.cleared_providers,
|
||||
"cleared_endpoints": cleanup.cleared_endpoints,
|
||||
"cleared_keys": cleanup.cleared_keys,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_delete_proxy_node_message(cleanup: &DeletedProxyNodeCleanup) -> String {
|
||||
let mut parts = vec!["deleted".to_string()];
|
||||
if cleanup.cleared_system_proxy {
|
||||
parts.push("system default proxy cleared".to_string());
|
||||
}
|
||||
if cleanup.cleared_external_models_proxy {
|
||||
parts.push("external models proxy cleared".to_string());
|
||||
}
|
||||
if cleanup.external_models_cache_clear_succeeded == Some(false) {
|
||||
parts.push("external models cache invalidation deferred".to_string());
|
||||
}
|
||||
if cleanup.cleared_providers > 0 || cleanup.cleared_endpoints > 0 || cleanup.cleared_keys > 0 {
|
||||
parts.push(format!(
|
||||
"cleared proxy refs from {} provider(s), {} endpoint(s), {} key(s)",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::handlers::admin::model::ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::shared::unix_secs_to_rfc3339;
|
||||
use crate::GatewayError;
|
||||
@@ -17,6 +18,30 @@ use axum::body::Bytes;
|
||||
use axum::http;
|
||||
use serde_json::json;
|
||||
|
||||
const ADMIN_EXTERNAL_MODELS_CONFIG_ROUTE: &str = "/api/admin/models/external/config";
|
||||
|
||||
fn is_external_models_proxy_node_config_key(key: &str) -> bool {
|
||||
key.trim()
|
||||
.eq_ignore_ascii_case(ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY)
|
||||
}
|
||||
|
||||
fn external_models_proxy_node_config_owner_error(
|
||||
key: &str,
|
||||
) -> Option<(http::StatusCode, serde_json::Value)> {
|
||||
is_external_models_proxy_node_config_key(key).then(|| {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
json!({
|
||||
"detail": format!(
|
||||
"配置项 '{}' 由模型目录管理,请使用 {}",
|
||||
ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY,
|
||||
ADMIN_EXTERNAL_MODELS_CONFIG_ROUTE,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_admin_system_config_key(requested_key: &str) -> String {
|
||||
normalize_admin_system_config_key_pure(requested_key)
|
||||
}
|
||||
@@ -51,7 +76,12 @@ fn legacy_admin_system_config_fallback_key(normalized_key: &str) -> Option<&'sta
|
||||
pub(crate) fn build_admin_system_configs_payload(
|
||||
entries: &[aether_data::repository::system::StoredSystemConfigEntry],
|
||||
) -> serde_json::Value {
|
||||
build_admin_system_configs_payload_pure(entries)
|
||||
let visible_entries = entries
|
||||
.iter()
|
||||
.filter(|entry| !is_external_models_proxy_node_config_key(&entry.key))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
build_admin_system_configs_payload_pure(&visible_entries)
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_system_config_detail_payload(
|
||||
@@ -59,6 +89,9 @@ pub(crate) async fn build_admin_system_config_detail_payload(
|
||||
requested_key: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
let requested_key = requested_key.trim();
|
||||
if let Some(error) = external_models_proxy_node_config_owner_error(requested_key) {
|
||||
return Ok(Err(error));
|
||||
}
|
||||
let normalized_key = normalize_admin_system_config_key(requested_key);
|
||||
let mut value = state.read_system_config_json_value(&normalized_key).await?;
|
||||
if value.is_none() {
|
||||
@@ -78,6 +111,9 @@ pub(crate) async fn apply_admin_system_config_update(
|
||||
requested_key: &str,
|
||||
request_body: &Bytes,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if let Some(error) = external_models_proxy_node_config_owner_error(requested_key) {
|
||||
return Ok(Err(error));
|
||||
}
|
||||
let update = match parse_admin_system_config_update(requested_key, request_body) {
|
||||
Ok(update) => update,
|
||||
Err(err) => return Ok(Err(err)),
|
||||
@@ -123,6 +159,9 @@ pub(crate) async fn delete_admin_system_config(
|
||||
state: &AdminAppState<'_>,
|
||||
requested_key: &str,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if let Some(error) = external_models_proxy_node_config_owner_error(requested_key) {
|
||||
return Ok(Err(error));
|
||||
}
|
||||
let delete_keys = admin_system_config_delete_keys(requested_key);
|
||||
let mut deleted = false;
|
||||
for key in &delete_keys {
|
||||
|
||||
Reference in New Issue
Block a user