mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
Merge remote-tracking branch 'origin/aether-rust-pioneer' into payment-billing-plans
# Conflicts: # crates/aether-data/src/lifecycle/bootstrap/postgres.rs # crates/aether-data/src/lifecycle/migrate/tests.rs
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
use super::shared::{
|
||||
admin_api_key_install_session_id_from_path, build_admin_api_keys_bad_request_response,
|
||||
build_admin_api_keys_data_unavailable_response, build_admin_api_keys_not_found_response,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::handlers::public::{
|
||||
build_api_key_install_session_response, CreateApiKeyInstallSessionRequest,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
pub(super) async fn build_admin_create_api_key_install_session_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if !state.has_auth_api_key_data_reader() {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
}
|
||||
|
||||
let Some(api_key_id) = admin_api_key_install_session_id_from_path(request_context.path())
|
||||
else {
|
||||
return Ok(build_admin_api_keys_data_unavailable_response());
|
||||
};
|
||||
let Some(request_body) = request_body else {
|
||||
return Ok(build_admin_api_keys_bad_request_response(
|
||||
"请求数据验证失败",
|
||||
));
|
||||
};
|
||||
let payload = match serde_json::from_slice::<CreateApiKeyInstallSessionRequest>(request_body) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return Ok(build_admin_api_keys_bad_request_response(
|
||||
"请求数据验证失败",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let Some(record) = state
|
||||
.find_auth_api_key_export_standalone_record_by_id(&api_key_id)
|
||||
.await?
|
||||
else {
|
||||
return Ok(build_admin_api_keys_not_found_response());
|
||||
};
|
||||
let Some(ciphertext) = record
|
||||
.key_encrypted
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return Ok(build_admin_api_keys_bad_request_response(
|
||||
"该密钥没有存储完整密钥信息",
|
||||
));
|
||||
};
|
||||
let Some(api_key) = state.decrypt_catalog_secret_with_fallbacks(ciphertext) else {
|
||||
return Ok((
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
axum::Json(serde_json::json!({ "detail": "解密密钥失败" })),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
|
||||
let response = build_api_key_install_session_response(
|
||||
state.app(),
|
||||
request_context.public(),
|
||||
request_headers,
|
||||
record.api_key_id.clone(),
|
||||
record.name.unwrap_or_else(|| "API Key".to_string()),
|
||||
api_key,
|
||||
payload,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(attach_admin_audit_response(
|
||||
response,
|
||||
"admin_standalone_api_key_install_session_created",
|
||||
"create_standalone_api_key_install_session",
|
||||
"api_key",
|
||||
&api_key_id,
|
||||
))
|
||||
}
|
||||
@@ -18,11 +18,13 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
mod install_routes;
|
||||
mod mutation_routes;
|
||||
mod read_routes;
|
||||
mod routes;
|
||||
mod shared;
|
||||
|
||||
use self::install_routes::build_admin_create_api_key_install_session_response;
|
||||
use self::mutation_routes::{
|
||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||
@@ -39,8 +41,14 @@ use self::shared::{
|
||||
pub(crate) async fn maybe_build_local_admin_api_keys_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_api_keys_routes_response(state, request_context, request_body)
|
||||
.await
|
||||
routes::maybe_build_local_admin_api_keys_routes_response(
|
||||
state,
|
||||
request_context,
|
||||
request_headers,
|
||||
request_body,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::install_routes::build_admin_create_api_key_install_session_response;
|
||||
use super::mutation_routes::{
|
||||
build_admin_create_api_key_response, build_admin_delete_api_key_response,
|
||||
build_admin_toggle_api_key_response, build_admin_update_api_key_response,
|
||||
@@ -11,6 +12,7 @@ use axum::{body::Body, http, response::Response};
|
||||
pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
request_headers: &http::HeaderMap,
|
||||
request_body: Option<&axum::body::Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.decision() else {
|
||||
@@ -22,8 +24,10 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
||||
}
|
||||
|
||||
let path = request_context.path();
|
||||
let path_no_trailing = path.trim_end_matches('/');
|
||||
let is_api_keys_route = matches!(path, "/api/admin/api-keys" | "/api/admin/api-keys/")
|
||||
|| (path.starts_with("/api/admin/api-keys/") && path.matches('/').count() == 4);
|
||||
|| (path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||
&& matches!(path_no_trailing.matches('/').count(), 4 | 5));
|
||||
|
||||
if !is_api_keys_route {
|
||||
return Ok(None);
|
||||
@@ -54,6 +58,21 @@ pub(super) async fn maybe_build_local_admin_api_keys_routes_response(
|
||||
build_admin_create_api_key_response(state, request_context, request_body).await?,
|
||||
))
|
||||
}
|
||||
Some("create_api_key_install_session")
|
||||
if request_context.method() == http::Method::POST
|
||||
&& path_no_trailing.starts_with("/api/admin/api-keys/")
|
||||
&& path_no_trailing.ends_with("/install-sessions") =>
|
||||
{
|
||||
Ok(Some(
|
||||
build_admin_create_api_key_install_session_response(
|
||||
state,
|
||||
request_context,
|
||||
request_headers,
|
||||
request_body,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
Some("update_api_key")
|
||||
if request_context.method() == http::Method::PUT
|
||||
&& path.starts_with("/api/admin/api-keys/") =>
|
||||
|
||||
@@ -91,6 +91,17 @@ pub(super) fn admin_api_keys_id_from_path(request_path: &str) -> Option<String>
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_api_key_install_session_id_from_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path
|
||||
.strip_prefix("/api/admin/api-keys/")?
|
||||
.trim()
|
||||
.trim_matches('/');
|
||||
let mut segments = raw.split('/').map(str::trim);
|
||||
let api_key_id = segments.next()?.to_string();
|
||||
let suffix = segments.next()?;
|
||||
(suffix == "install-sessions" && segments.next().is_none()).then_some(api_key_id)
|
||||
}
|
||||
|
||||
pub(super) fn admin_api_keys_operator_id(
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Option<String> {
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) async fn maybe_build_local_admin_auth_response(
|
||||
if let Some(response) = api_keys::maybe_build_local_admin_api_keys_response(
|
||||
&request.state(),
|
||||
&request.request_context(),
|
||||
request.request_headers(),
|
||||
request.request_body(),
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -29,12 +29,14 @@ pub(crate) use self::provider::oauth::quota::antigravity::refresh_antigravity_pr
|
||||
pub(crate) use self::provider::oauth::quota::chatgpt_web::refresh_chatgpt_web_provider_quota_locally;
|
||||
pub(crate) use self::provider::oauth::quota::codex::refresh_codex_provider_quota_locally;
|
||||
pub(crate) use self::provider::oauth::quota::kiro::refresh_kiro_provider_quota_locally;
|
||||
pub(crate) use self::provider::oauth::quota::shared::provider_type_supports_quota_refresh;
|
||||
pub(crate) use self::provider::oauth::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider, refresh_provider_oauth_account_state_after_update,
|
||||
};
|
||||
pub(crate) use self::provider::ops::providers::actions::admin_provider_ops_local_action_response;
|
||||
pub(crate) use self::provider::pool::config::admin_provider_pool_config;
|
||||
pub(crate) use self::provider::pool_admin::maybe_build_local_admin_pool_response;
|
||||
pub(crate) use self::provider::write::provider::reconcile_admin_fixed_provider_template_endpoints;
|
||||
pub(crate) use self::provider::{
|
||||
maybe_build_local_admin_provider_oauth_response, maybe_build_local_admin_providers_response,
|
||||
};
|
||||
|
||||
@@ -265,8 +265,8 @@ async fn admin_monitoring_cache_stats_count_runtime_scheduler_affinities() {
|
||||
"model-alpha",
|
||||
)
|
||||
.expect("scheduler affinity cache key should build");
|
||||
state.scheduler_affinity_cache.insert(
|
||||
affinity_cache_key,
|
||||
state.remember_scheduler_affinity_target(
|
||||
&affinity_cache_key,
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
provider_id: "provider-1".to_string(),
|
||||
endpoint_id: "endpoint-1".to_string(),
|
||||
|
||||
@@ -239,8 +239,8 @@ async fn admin_monitoring_cache_affinities_and_delete_use_runtime_scheduler_affi
|
||||
"model-alpha",
|
||||
)
|
||||
.expect("scheduler affinity cache key should build");
|
||||
state.scheduler_affinity_cache.insert(
|
||||
affinity_cache_key.clone(),
|
||||
state.remember_scheduler_affinity_target(
|
||||
&affinity_cache_key,
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
provider_id: "provider-1".to_string(),
|
||||
endpoint_id: "endpoint-1".to_string(),
|
||||
@@ -393,8 +393,8 @@ async fn admin_monitoring_cache_affinities_parse_session_scoped_scheduler_affini
|
||||
.next()
|
||||
.expect("session hash should exist")
|
||||
.to_string();
|
||||
state.scheduler_affinity_cache.insert(
|
||||
affinity_cache_key.clone(),
|
||||
state.remember_scheduler_affinity_target(
|
||||
&affinity_cache_key,
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
provider_id: "provider-1".to_string(),
|
||||
endpoint_id: "endpoint-1".to_string(),
|
||||
@@ -403,8 +403,8 @@ async fn admin_monitoring_cache_affinities_parse_session_scoped_scheduler_affini
|
||||
crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL,
|
||||
128,
|
||||
);
|
||||
state.scheduler_affinity_cache.insert(
|
||||
other_affinity_cache_key.clone(),
|
||||
state.remember_scheduler_affinity_target(
|
||||
&other_affinity_cache_key,
|
||||
crate::cache::SchedulerAffinityTarget {
|
||||
provider_id: "provider-1".to_string(),
|
||||
endpoint_id: "endpoint-1".to_string(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::handlers::admin::admin_provider_pool_config;
|
||||
use crate::handlers::admin::provider::shared::paths::admin_provider_id_for_keys;
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyCreateRequest;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::maintenance::ensure_provider_key_pool_scores_for_keys;
|
||||
use crate::provider_key_auth::provider_key_effective_api_formats;
|
||||
use crate::{model_fetch::perform_model_fetch_for_key, GatewayError};
|
||||
use axum::{
|
||||
@@ -98,6 +100,27 @@ pub(super) async fn maybe_handle(
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
if let Some(pool_config) = admin_provider_pool_config(&provider) {
|
||||
let score_ensure_budget = (pool_config.score_fallback_scan_limit as usize).clamp(1, 50_000);
|
||||
if let Err(err) = ensure_provider_key_pool_scores_for_keys(
|
||||
state.as_ref(),
|
||||
&provider,
|
||||
&pool_config,
|
||||
&endpoints,
|
||||
std::slice::from_ref(&created),
|
||||
now_unix_secs,
|
||||
score_ensure_budget,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
provider_id = %provider.id,
|
||||
key_id = %created.id,
|
||||
error = ?err,
|
||||
"gateway admin provider key create: failed to seed pool score rows"
|
||||
);
|
||||
}
|
||||
}
|
||||
let api_formats =
|
||||
provider_key_effective_api_formats(&created, &provider.provider_type, &endpoints);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::handlers::admin::admin_provider_pool_config;
|
||||
use crate::handlers::admin::provider::shared::paths::admin_update_key_id;
|
||||
use crate::handlers::admin::provider::shared::payloads::AdminProviderKeyUpdatePatch;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::maintenance::ensure_provider_key_pool_scores_for_keys;
|
||||
use crate::provider_key_auth::provider_key_effective_api_formats;
|
||||
use crate::{model_fetch::perform_model_fetch_for_key, GatewayError};
|
||||
use axum::{
|
||||
@@ -121,6 +123,27 @@ pub(super) async fn maybe_handle(
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
if let Some(pool_config) = admin_provider_pool_config(&provider) {
|
||||
let score_ensure_budget = (pool_config.score_fallback_scan_limit as usize).clamp(1, 50_000);
|
||||
if let Err(err) = ensure_provider_key_pool_scores_for_keys(
|
||||
state.as_ref(),
|
||||
&provider,
|
||||
&pool_config,
|
||||
&endpoints,
|
||||
std::slice::from_ref(&updated),
|
||||
now_unix_secs,
|
||||
score_ensure_budget,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::debug!(
|
||||
provider_id = %provider.id,
|
||||
key_id = %updated.id,
|
||||
error = ?err,
|
||||
"gateway admin provider key update: failed to seed pool score rows"
|
||||
);
|
||||
}
|
||||
}
|
||||
let api_formats =
|
||||
provider_key_effective_api_formats(&updated, &provider.provider_type, &endpoints);
|
||||
|
||||
|
||||
@@ -18,6 +18,24 @@ use super::super::oauth::quota::chatgpt_web::refresh_chatgpt_web_provider_quota_
|
||||
use super::super::oauth::quota::codex::refresh_codex_provider_quota_locally;
|
||||
use super::super::oauth::quota::kiro::refresh_kiro_provider_quota_locally;
|
||||
use super::super::oauth::quota::shared::normalize_string_id_list;
|
||||
use super::super::oauth::quota::shared::{
|
||||
provider_type_supports_quota_refresh, unsupported_provider_quota_refresh_message,
|
||||
};
|
||||
use super::super::oauth::runtime::provider_oauth_runtime_endpoint_for_provider;
|
||||
use super::super::write::provider::reconcile_admin_fixed_provider_template_endpoints;
|
||||
|
||||
fn unsupported_provider_quota_refresh_response(provider_type: &str) -> Response<Body> {
|
||||
let message = unsupported_provider_quota_refresh_message(provider_type);
|
||||
Json(json!({
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": 0,
|
||||
"results": [],
|
||||
"message": message,
|
||||
"auto_removed": 0,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_handle(
|
||||
state: &AdminAppState<'_>,
|
||||
@@ -85,41 +103,48 @@ pub(super) async fn maybe_handle(
|
||||
let raw_key_ids = payload.key_ids;
|
||||
let selected_key_ids = normalize_string_id_list(raw_key_ids.clone());
|
||||
let explicit_key_ids_requested = raw_key_ids.is_some();
|
||||
let endpoints = state
|
||||
|
||||
let is_fixed_provider = state
|
||||
.fixed_provider_template(&provider.provider_type)
|
||||
.is_some();
|
||||
if !is_fixed_provider && !provider_type_supports_quota_refresh(&normalized_provider_type) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let endpoint = match normalized_provider_type.as_str() {
|
||||
"codex" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& crate::ai_serving::is_openai_responses_format(&endpoint.api_format)
|
||||
}),
|
||||
"antigravity" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
}),
|
||||
"kiro" => endpoints
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| endpoints.into_iter().find(|endpoint| endpoint.is_active)),
|
||||
"chatgpt_web" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:image")
|
||||
}),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let mut endpoint =
|
||||
provider_oauth_runtime_endpoint_for_provider(&normalized_provider_type, &endpoints);
|
||||
|
||||
if endpoint.is_none() && is_fixed_provider {
|
||||
if !state.has_provider_catalog_data_writer() {
|
||||
if !provider_type_supports_quota_refresh(&normalized_provider_type) {
|
||||
return Ok(Some(unsupported_provider_quota_refresh_response(
|
||||
&normalized_provider_type,
|
||||
)));
|
||||
}
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "固定 Provider 端点缺失,且 provider catalog writer 不可用,无法自动补全端点" })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
reconcile_admin_fixed_provider_template_endpoints(state, &provider).await?;
|
||||
endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
endpoint =
|
||||
provider_oauth_runtime_endpoint_for_provider(&normalized_provider_type, &endpoints);
|
||||
}
|
||||
|
||||
if !provider_type_supports_quota_refresh(&normalized_provider_type) {
|
||||
return Ok(Some(unsupported_provider_quota_refresh_response(
|
||||
&normalized_provider_type,
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(endpoint) = endpoint else {
|
||||
let detail = match normalized_provider_type.as_str() {
|
||||
@@ -127,6 +152,8 @@ pub(super) async fn maybe_handle(
|
||||
"antigravity" => "找不到有效的 gemini:generate_content 端点",
|
||||
"kiro" => "找不到有效的 Kiro 端点",
|
||||
"chatgpt_web" => "找不到有效的 openai:image 端点",
|
||||
"claude_code" => "找不到有效的 claude:messages 端点",
|
||||
"gemini_cli" | "vertex_ai" => "找不到有效的 gemini:generate_content 端点",
|
||||
_ => "找不到有效端点",
|
||||
};
|
||||
return Ok(Some(
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::handlers::admin::provider::oauth::provisioning::{
|
||||
provider_oauth_key_proxy_value, update_existing_provider_oauth_catalog_key,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider,
|
||||
resolve_provider_oauth_runtime_endpoints,
|
||||
spawn_provider_oauth_account_state_refresh_after_update,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::state::{
|
||||
@@ -224,11 +224,11 @@ pub(super) async fn execute_admin_provider_oauth_batch_import(
|
||||
});
|
||||
};
|
||||
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&[provider_id.to_string()])
|
||||
.await?;
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, provider_type).await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let api_formats = provider_oauth_active_api_formats(&endpoints);
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider(provider_type, &endpoints);
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
proxy_node_id,
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::handlers::admin::provider::oauth::provisioning::{
|
||||
provider_oauth_key_proxy_value, update_existing_provider_oauth_catalog_key,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider,
|
||||
resolve_provider_oauth_runtime_endpoints,
|
||||
spawn_provider_oauth_account_state_refresh_after_update,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::state::decode_jwt_claims;
|
||||
@@ -59,11 +59,11 @@ pub(super) async fn execute_admin_provider_oauth_kiro_batch_import(
|
||||
});
|
||||
};
|
||||
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&[provider_id.to_string()])
|
||||
.await?;
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, "kiro").await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let api_formats = provider_oauth_active_api_formats(&endpoints);
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider("kiro", &endpoints);
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
proxy_node_id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::super::errors::build_internal_control_error_response;
|
||||
use super::super::super::provisioning::provider_oauth_token_payload_expires_at_unix_secs;
|
||||
use super::super::super::quota::codex::refresh_codex_provider_quota_locally;
|
||||
use super::super::super::runtime::provider_oauth_runtime_endpoint_for_provider;
|
||||
use super::super::super::runtime::resolve_provider_oauth_runtime_endpoints;
|
||||
use super::super::super::state::{
|
||||
admin_provider_oauth_template, enrich_admin_provider_oauth_auth_config,
|
||||
is_fixed_provider_type_for_provider_oauth, json_non_empty_string,
|
||||
@@ -126,10 +126,10 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
|
||||
"该 Provider 不支持 OAuth 授权",
|
||||
));
|
||||
};
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider(&provider_type, &endpoints);
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
payload.proxy_node_id.as_deref(),
|
||||
@@ -223,9 +223,6 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
|
||||
let mut account_state_recheck_attempted = false;
|
||||
let mut account_state_recheck_error = None::<String>;
|
||||
if provider_type == "codex" {
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
if let Some(endpoint) = endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& crate::ai_serving::is_openai_responses_format(&endpoint.api_format)
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::super::super::provisioning::{
|
||||
update_existing_provider_oauth_catalog_key,
|
||||
};
|
||||
use super::super::super::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider,
|
||||
resolve_provider_oauth_runtime_endpoints,
|
||||
spawn_provider_oauth_account_state_refresh_after_update,
|
||||
};
|
||||
use super::super::super::state::{
|
||||
@@ -117,10 +117,10 @@ pub(super) async fn handle_admin_provider_oauth_complete_provider(
|
||||
"该 Provider 不支持 OAuth 授权",
|
||||
));
|
||||
};
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider(&provider_type, &endpoints);
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
payload.proxy_node_id.as_deref(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::session::AdminProviderOAuthDeviceAuthorizePayload;
|
||||
use crate::handlers::admin::provider::oauth::errors::build_internal_control_error_response;
|
||||
use crate::handlers::admin::provider::oauth::runtime::provider_oauth_runtime_endpoint_for_provider;
|
||||
use crate::handlers::admin::provider::oauth::runtime::resolve_provider_oauth_runtime_endpoints;
|
||||
use crate::handlers::admin::provider::oauth::state::{
|
||||
build_admin_provider_oauth_backend_unavailable_response, current_unix_secs,
|
||||
default_kiro_device_start_url, generate_provider_oauth_nonce, json_non_empty_string,
|
||||
@@ -169,10 +169,9 @@ pub(super) async fn handle_admin_provider_oauth_device_authorize(
|
||||
"设备授权仅支持 Kiro provider",
|
||||
));
|
||||
}
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider("kiro", &endpoints);
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, "kiro").await?;
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
payload.proxy_node_id.as_deref(),
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::handlers::admin::provider::oauth::provisioning::{
|
||||
provider_oauth_active_api_formats, provider_oauth_key_proxy_value,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider,
|
||||
resolve_provider_oauth_runtime_endpoints,
|
||||
spawn_provider_oauth_account_state_refresh_after_update,
|
||||
};
|
||||
use crate::handlers::admin::provider::oauth::state::{
|
||||
@@ -322,10 +322,10 @@ pub(super) async fn handle_admin_provider_oauth_device_poll(
|
||||
"Provider 不存在",
|
||||
));
|
||||
};
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider("kiro", &endpoints);
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, "kiro").await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
session.proxy_node_id.as_deref(),
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::super::provisioning::{
|
||||
update_existing_provider_oauth_catalog_key,
|
||||
};
|
||||
use super::super::runtime::{
|
||||
provider_oauth_runtime_endpoint_for_provider,
|
||||
resolve_provider_oauth_runtime_endpoints,
|
||||
spawn_provider_oauth_account_state_refresh_after_update,
|
||||
};
|
||||
use super::super::state::{
|
||||
@@ -300,10 +300,10 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
|
||||
let Some(template) = admin_provider_oauth_template(&provider_type) else {
|
||||
return Ok(build_admin_provider_oauth_backend_unavailable_response());
|
||||
};
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let runtime_endpoint = provider_oauth_runtime_endpoint_for_provider(&provider_type, &endpoints);
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
|
||||
let endpoints = endpoint_resolution.endpoints;
|
||||
let runtime_endpoint = endpoint_resolution.runtime_endpoint;
|
||||
let request_proxy = state
|
||||
.resolve_admin_provider_oauth_operation_proxy_snapshot(
|
||||
proxy_node_id.as_deref(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::super::super::runtime::provider_oauth_runtime_endpoint_for_provider;
|
||||
use super::super::super::runtime::resolve_provider_oauth_runtime_endpoints;
|
||||
use super::super::super::state::is_fixed_provider_type_for_provider_oauth;
|
||||
use super::helpers::{self, RefreshDispatch, RefreshRequestContext};
|
||||
use super::response;
|
||||
@@ -76,11 +76,19 @@ pub(super) async fn parse_admin_provider_oauth_refresh_request(
|
||||
)));
|
||||
}
|
||||
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
|
||||
.await?;
|
||||
let Some(endpoint) = provider_oauth_runtime_endpoint_for_provider(&provider_type, &endpoints)
|
||||
else {
|
||||
let endpoint_resolution =
|
||||
resolve_provider_oauth_runtime_endpoints(state, &provider, &provider_type).await?;
|
||||
let Some(endpoint) = endpoint_resolution.runtime_endpoint else {
|
||||
if state
|
||||
.fixed_provider_template(&provider.provider_type)
|
||||
.is_some()
|
||||
&& !state.has_provider_catalog_data_writer()
|
||||
{
|
||||
return Ok(RefreshDispatch::Respond(response::control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"固定 Provider 端点缺失,且 provider catalog writer 不可用,无法自动补全端点",
|
||||
)));
|
||||
}
|
||||
return Ok(RefreshDispatch::Respond(response::control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"找不到有效端点,无法 refresh",
|
||||
|
||||
@@ -50,6 +50,25 @@ pub(crate) fn normalize_string_id_list(values: Option<Vec<String>>) -> Option<Ve
|
||||
admin_provider_quota_pure::normalize_string_id_list(values)
|
||||
}
|
||||
|
||||
pub(crate) fn provider_type_supports_quota_refresh(provider_type: &str) -> bool {
|
||||
matches!(
|
||||
provider_type.trim().to_ascii_lowercase().as_str(),
|
||||
"codex" | "kiro" | "antigravity" | "chatgpt_web"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn unsupported_provider_quota_refresh_message(provider_type: &str) -> String {
|
||||
match provider_type.trim().to_ascii_lowercase().as_str() {
|
||||
"claude_code" => "Claude Code 暂不支持自动刷新额度:上游没有稳定可用的账号额度查询接口",
|
||||
"gemini_cli" => {
|
||||
"Gemini CLI 暂不支持自动刷新额度:当前只能通过模型同步/缓存快照展示已知配额信息"
|
||||
}
|
||||
"vertex_ai" => "Vertex AI 暂不支持自动刷新额度:额度属于 Google Cloud 项目/区域配额",
|
||||
_ => "该 Provider 暂不支持自动刷新额度",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(super) fn coerce_json_u64(value: &serde_json::Value) -> Option<u64> {
|
||||
admin_provider_quota_pure::coerce_json_u64(value)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::quota::antigravity::refresh_antigravity_provider_quota_locally;
|
||||
use super::quota::chatgpt_web::refresh_chatgpt_web_provider_quota_locally;
|
||||
use super::quota::codex::refresh_codex_provider_quota_locally;
|
||||
use super::quota::kiro::refresh_kiro_provider_quota_locally;
|
||||
use crate::handlers::admin::provider::write::provider::reconcile_admin_fixed_provider_template_endpoints;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::provider_key_auth::provider_key_is_oauth_managed;
|
||||
use crate::task_runtime::{spawn_fire_and_forget, TASK_KEY_PROVIDER_OAUTH_ACCOUNT_REFRESH};
|
||||
@@ -60,6 +61,48 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
|
||||
.find(|endpoint| endpoint.is_active)
|
||||
.cloned()
|
||||
}),
|
||||
"claude_code" => endpoints
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
})
|
||||
.cloned(),
|
||||
"gemini_cli" => endpoints
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
})
|
||||
.cloned(),
|
||||
"vertex_ai" => endpoints
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("gemini:generate_content")
|
||||
})
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
endpoints
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("claude:messages")
|
||||
})
|
||||
.cloned()
|
||||
}),
|
||||
_ => endpoints
|
||||
.iter()
|
||||
.find(|endpoint| endpoint.is_active)
|
||||
@@ -67,6 +110,41 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ProviderOAuthRuntimeEndpoints {
|
||||
pub(crate) endpoints: Vec<StoredProviderCatalogEndpoint>,
|
||||
pub(crate) runtime_endpoint: Option<StoredProviderCatalogEndpoint>,
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_provider_oauth_runtime_endpoints(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
provider_type: &str,
|
||||
) -> Result<ProviderOAuthRuntimeEndpoints, GatewayError> {
|
||||
let mut endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
let mut runtime_endpoint =
|
||||
provider_oauth_runtime_endpoint_for_provider(provider_type, &endpoints);
|
||||
if runtime_endpoint.is_none()
|
||||
&& state
|
||||
.fixed_provider_template(&provider.provider_type)
|
||||
.is_some()
|
||||
&& state.has_provider_catalog_data_writer()
|
||||
{
|
||||
reconcile_admin_fixed_provider_template_endpoints(state, provider).await?;
|
||||
endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
runtime_endpoint = provider_oauth_runtime_endpoint_for_provider(provider_type, &endpoints);
|
||||
}
|
||||
|
||||
Ok(ProviderOAuthRuntimeEndpoints {
|
||||
endpoints,
|
||||
runtime_endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn refresh_provider_oauth_account_state_after_update(
|
||||
state: &AdminAppState<'_>,
|
||||
provider: &StoredProviderCatalogProvider,
|
||||
@@ -81,11 +159,10 @@ pub(crate) async fn refresh_provider_oauth_account_state_after_update(
|
||||
return Ok((false, None));
|
||||
}
|
||||
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
let Some(endpoint) = provider_oauth_runtime_endpoint_for_provider(&provider_type, &endpoints)
|
||||
else {
|
||||
let ProviderOAuthRuntimeEndpoints {
|
||||
runtime_endpoint, ..
|
||||
} = resolve_provider_oauth_runtime_endpoints(state, provider, &provider_type).await?;
|
||||
let Some(endpoint) = runtime_endpoint else {
|
||||
return Ok((false, None));
|
||||
};
|
||||
let Some(key) = state
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::handlers::admin::provider::shared::support::{
|
||||
AdminProviderPoolConfig, AdminProviderPoolSchedulingPreset, AdminProviderPoolUnschedulableRule,
|
||||
};
|
||||
use aether_ai_serving::{PoolMemberScoreRules, PoolMemberScoreWeights};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
const POOL_ALLOWED_SCHEDULING_PRESETS: &[&str] = &[
|
||||
@@ -26,6 +27,123 @@ fn json_u64(value: &Value) -> Option<u64> {
|
||||
.or_else(|| value.as_i64().and_then(|raw| u64::try_from(raw).ok()))
|
||||
}
|
||||
|
||||
fn json_f64(value: &Value) -> Option<f64> {
|
||||
value.as_f64().or_else(|| {
|
||||
value
|
||||
.as_str()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.and_then(|value| value.parse::<f64>().ok())
|
||||
})
|
||||
}
|
||||
|
||||
fn pool_score_weight(object: &Map<String, Value>, names: &[&str], current: f64) -> f64 {
|
||||
names
|
||||
.iter()
|
||||
.find_map(|name| {
|
||||
object
|
||||
.get(*name)
|
||||
.and_then(json_f64)
|
||||
.filter(|value| value.is_finite() && *value >= 0.0)
|
||||
})
|
||||
.unwrap_or(current)
|
||||
}
|
||||
|
||||
fn parse_pool_score_weights(
|
||||
raw_weights: Option<&Map<String, Value>>,
|
||||
current: PoolMemberScoreWeights,
|
||||
) -> PoolMemberScoreWeights {
|
||||
let Some(raw_weights) = raw_weights else {
|
||||
return current;
|
||||
};
|
||||
PoolMemberScoreWeights {
|
||||
manual_priority: pool_score_weight(
|
||||
raw_weights,
|
||||
&["manual_priority", "priority", "internal_priority"],
|
||||
current.manual_priority,
|
||||
),
|
||||
health: pool_score_weight(raw_weights, &["health"], current.health),
|
||||
probe_freshness: pool_score_weight(
|
||||
raw_weights,
|
||||
&["probe_freshness", "freshness", "probe"],
|
||||
current.probe_freshness,
|
||||
),
|
||||
quota_remaining: pool_score_weight(
|
||||
raw_weights,
|
||||
&["quota_remaining", "quota", "quota_available"],
|
||||
current.quota_remaining,
|
||||
),
|
||||
latency: pool_score_weight(raw_weights, &["latency"], current.latency),
|
||||
cost_lru: pool_score_weight(
|
||||
raw_weights,
|
||||
&["cost_lru", "cost_remaining", "cost", "lru"],
|
||||
current.cost_lru,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pool_score_rules(pool_advanced: &Map<String, Value>) -> PoolMemberScoreRules {
|
||||
let mut rules = PoolMemberScoreRules::default();
|
||||
|
||||
for key in ["score_weights", "pool_score_weights", "scoring_weights"] {
|
||||
rules.weights = parse_pool_score_weights(
|
||||
pool_advanced.get(key).and_then(Value::as_object),
|
||||
rules.weights,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(score_rules) = pool_advanced
|
||||
.get("score_rules")
|
||||
.or_else(|| pool_advanced.get("pool_score_rules"))
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
rules.weights = parse_pool_score_weights(
|
||||
score_rules.get("weights").and_then(Value::as_object),
|
||||
rules.weights,
|
||||
);
|
||||
if let Some(ttl_seconds) = score_rules
|
||||
.get("probe_freshness_ttl_seconds")
|
||||
.or_else(|| score_rules.get("score_probe_freshness_ttl_seconds"))
|
||||
.and_then(json_u64)
|
||||
.filter(|value| *value > 0)
|
||||
{
|
||||
rules.probe_freshness_ttl_seconds = ttl_seconds.min(7 * 24 * 3600);
|
||||
}
|
||||
if let Some(cap) = score_rules
|
||||
.get("unschedulable_score_cap")
|
||||
.or_else(|| score_rules.get("hard_state_score_cap"))
|
||||
.and_then(json_f64)
|
||||
.filter(|value| value.is_finite())
|
||||
{
|
||||
rules.unschedulable_score_cap = cap.clamp(0.0, 1.0);
|
||||
}
|
||||
if let Some(penalty) = score_rules
|
||||
.get("probe_failure_penalty")
|
||||
.and_then(json_f64)
|
||||
.filter(|value| value.is_finite())
|
||||
{
|
||||
rules.probe_failure_penalty = penalty.clamp(0.0, 1.0);
|
||||
}
|
||||
if let Some(penalty) = score_rules
|
||||
.get("request_failure_penalty")
|
||||
.or_else(|| score_rules.get("runtime_failure_penalty"))
|
||||
.and_then(json_f64)
|
||||
.filter(|value| value.is_finite())
|
||||
{
|
||||
rules.request_failure_penalty = penalty.clamp(0.0, 1.0);
|
||||
}
|
||||
if let Some(threshold) = score_rules
|
||||
.get("probe_failure_cooldown_threshold")
|
||||
.or_else(|| score_rules.get("probe_failure_hard_state_threshold"))
|
||||
.and_then(json_u64)
|
||||
{
|
||||
rules.probe_failure_cooldown_threshold = threshold.min(100);
|
||||
}
|
||||
}
|
||||
|
||||
rules.effective()
|
||||
}
|
||||
|
||||
fn normalize_pool_preset_mode(preset: &str, raw_mode: Option<&Value>) -> Option<String> {
|
||||
match preset {
|
||||
"free_first" | "team_first" | "plus_first" | "pro_first" => {
|
||||
@@ -270,6 +388,10 @@ pub(crate) fn admin_provider_pool_config_from_config_value(
|
||||
health_policy_enabled: true,
|
||||
probing_enabled: false,
|
||||
probing_interval_minutes: 10,
|
||||
probe_concurrency: 4,
|
||||
score_top_n: 128,
|
||||
score_fallback_scan_limit: 1024,
|
||||
score_rules: PoolMemberScoreRules::default(),
|
||||
stream_timeout_threshold: 3,
|
||||
stream_timeout_window_seconds: 1800,
|
||||
stream_timeout_cooldown_seconds: 300,
|
||||
@@ -278,6 +400,7 @@ pub(crate) fn admin_provider_pool_config_from_config_value(
|
||||
|
||||
let scheduling_presets = parse_pool_scheduling_presets(pool_advanced);
|
||||
let unschedulable_rules = parse_pool_unschedulable_rules(pool_advanced);
|
||||
let score_rules = parse_pool_score_rules(pool_advanced);
|
||||
|
||||
Some(AdminProviderPoolConfig {
|
||||
lru_enabled: admin_provider_pool_lru_enabled(&scheduling_presets),
|
||||
@@ -333,6 +456,25 @@ pub(crate) fn admin_provider_pool_config_from_config_value(
|
||||
.filter(|value| *value > 0)
|
||||
.map(|value| value.min(1440))
|
||||
.unwrap_or(10),
|
||||
probe_concurrency: pool_advanced
|
||||
.get("probe_concurrency")
|
||||
.and_then(json_u64)
|
||||
.filter(|value| *value > 0)
|
||||
.map(|value| value.min(64))
|
||||
.unwrap_or(4),
|
||||
score_top_n: pool_advanced
|
||||
.get("score_top_n")
|
||||
.and_then(json_u64)
|
||||
.filter(|value| *value > 0)
|
||||
.map(|value| value.min(4096))
|
||||
.unwrap_or(128),
|
||||
score_fallback_scan_limit: pool_advanced
|
||||
.get("score_fallback_scan_limit")
|
||||
.and_then(json_u64)
|
||||
.filter(|value| *value > 0)
|
||||
.map(|value| value.min(50_000))
|
||||
.unwrap_or(1024),
|
||||
score_rules,
|
||||
stream_timeout_threshold: pool_advanced
|
||||
.get("stream_timeout_threshold")
|
||||
.and_then(json_u64)
|
||||
@@ -405,6 +547,24 @@ mod tests {
|
||||
"health_policy_enabled": false,
|
||||
"probing_enabled": true,
|
||||
"probing_interval_minutes": 20,
|
||||
"probe_concurrency": 6,
|
||||
"score_top_n": 256,
|
||||
"score_fallback_scan_limit": 2048,
|
||||
"score_rules": {
|
||||
"weights": {
|
||||
"manual_priority": 0.4,
|
||||
"health": 0.2,
|
||||
"probe_freshness": 0.2,
|
||||
"quota_remaining": 0.1,
|
||||
"latency": 0.05,
|
||||
"cost_lru": 0.05
|
||||
},
|
||||
"probe_freshness_ttl_seconds": 1200,
|
||||
"unschedulable_score_cap": 0.03,
|
||||
"probe_failure_penalty": 0.08,
|
||||
"request_failure_penalty": 0.01,
|
||||
"probe_failure_cooldown_threshold": 2
|
||||
},
|
||||
"stream_timeout_threshold": 4,
|
||||
"stream_timeout_window_seconds": 900,
|
||||
"stream_timeout_cooldown_seconds": 180
|
||||
@@ -424,6 +584,16 @@ mod tests {
|
||||
assert!(!config.health_policy_enabled);
|
||||
assert!(config.probing_enabled);
|
||||
assert_eq!(config.probing_interval_minutes, 20);
|
||||
assert_eq!(config.probe_concurrency, 6);
|
||||
assert_eq!(config.score_top_n, 256);
|
||||
assert_eq!(config.score_fallback_scan_limit, 2048);
|
||||
assert_eq!(config.score_rules.weights.manual_priority, 0.4);
|
||||
assert_eq!(config.score_rules.weights.health, 0.2);
|
||||
assert_eq!(config.score_rules.probe_freshness_ttl_seconds, 1200);
|
||||
assert_eq!(config.score_rules.unschedulable_score_cap, 0.03);
|
||||
assert_eq!(config.score_rules.probe_failure_penalty, 0.08);
|
||||
assert_eq!(config.score_rules.request_failure_penalty, 0.01);
|
||||
assert_eq!(config.score_rules.probe_failure_cooldown_threshold, 2);
|
||||
assert_eq!(config.stream_timeout_threshold, 4);
|
||||
assert_eq!(config.stream_timeout_window_seconds, 900);
|
||||
assert_eq!(config.stream_timeout_cooldown_seconds, 180);
|
||||
@@ -462,6 +632,27 @@ mod tests {
|
||||
assert_eq!(config.sticky_session_ttl_seconds, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_legacy_pool_score_weights_from_pool_advanced() {
|
||||
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
||||
"pool_advanced": {
|
||||
"scoring_weights": {
|
||||
"manual_priority": 0,
|
||||
"health": 2,
|
||||
"probe": 1,
|
||||
"quota_remaining": 0,
|
||||
"latency": 0,
|
||||
"cost_remaining": 1
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect("pool config should parse");
|
||||
|
||||
assert_eq!(config.score_rules.weights.health, 0.5);
|
||||
assert_eq!(config.score_rules.weights.probe_freshness, 0.25);
|
||||
assert_eq!(config.score_rules.weights.cost_lru, 0.25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_pool_config_from_generic_config_value() {
|
||||
let config = admin_provider_pool_config_from_config_value(Some(&json!({
|
||||
|
||||
@@ -14,10 +14,6 @@ pub(super) fn pool_cooldown_key(provider_id: &str, key_id: &str) -> String {
|
||||
format!("ap:{provider_id}:cooldown:{key_id}")
|
||||
}
|
||||
|
||||
pub(super) fn pool_lease_key(provider_id: &str, key_id: &str) -> String {
|
||||
format!("ap:{provider_id}:lease:{key_id}")
|
||||
}
|
||||
|
||||
pub(super) fn pool_cooldown_index_key(provider_id: &str) -> String {
|
||||
format!("ap:{provider_id}:cooldown_idx")
|
||||
}
|
||||
|
||||
@@ -1,23 +1,4 @@
|
||||
use super::keys::pool_lease_key;
|
||||
use aether_runtime_state::{DataLayerError, RuntimeLockLease, RuntimeState};
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const ADMIN_PROVIDER_POOL_KEY_LEASE_TTL_MS: u64 = 15 * 60 * 1000;
|
||||
|
||||
pub(crate) async fn try_claim_admin_provider_pool_key(
|
||||
runtime: &RuntimeState,
|
||||
provider_id: &str,
|
||||
key_id: &str,
|
||||
owner: &str,
|
||||
) -> Result<Option<RuntimeLockLease>, DataLayerError> {
|
||||
runtime
|
||||
.lock_try_acquire(
|
||||
&pool_lease_key(provider_id, key_id),
|
||||
owner,
|
||||
Duration::from_millis(ADMIN_PROVIDER_POOL_KEY_LEASE_TTL_MS),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn release_admin_provider_pool_key_lease(
|
||||
runtime: &RuntimeState,
|
||||
|
||||
@@ -5,16 +5,14 @@ mod reads;
|
||||
mod status;
|
||||
mod writes;
|
||||
|
||||
pub(crate) use self::leases::{
|
||||
release_admin_provider_pool_key_lease, try_claim_admin_provider_pool_key,
|
||||
ADMIN_PROVIDER_POOL_KEY_LEASE_TTL_MS,
|
||||
};
|
||||
pub(crate) use self::leases::release_admin_provider_pool_key_lease;
|
||||
pub(crate) use self::mutations::{
|
||||
clear_admin_provider_pool_cooldown, reset_admin_provider_pool_cost,
|
||||
};
|
||||
pub(crate) use self::reads::{
|
||||
read_admin_provider_pool_cooldown_count, read_admin_provider_pool_cooldown_counts,
|
||||
read_admin_provider_pool_cooldown_key_ids, read_admin_provider_pool_runtime_state,
|
||||
read_admin_provider_pool_cooldown_key_ids, read_admin_provider_pool_key_cooldown_reason,
|
||||
read_admin_provider_pool_runtime_state,
|
||||
};
|
||||
pub(crate) use self::status::build_admin_provider_pool_status_payload;
|
||||
pub(crate) use self::writes::{
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::handlers::admin::provider::pool::config::admin_provider_pool_cache_af
|
||||
use crate::handlers::admin::provider::shared::support::{
|
||||
AdminProviderPoolConfig, AdminProviderPoolRuntimeState,
|
||||
};
|
||||
use aether_runtime_state::RuntimeState;
|
||||
use aether_runtime_state::{DataLayerError, RuntimeState};
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::warn;
|
||||
@@ -202,3 +202,13 @@ pub(crate) async fn read_admin_provider_pool_cooldown_key_ids(
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) async fn read_admin_provider_pool_key_cooldown_reason(
|
||||
runtime: &RuntimeState,
|
||||
provider_id: &str,
|
||||
key_id: &str,
|
||||
) -> Result<Option<String>, DataLayerError> {
|
||||
runtime
|
||||
.kv_get(&pool_cooldown_key(provider_id, key_id))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -615,6 +615,10 @@ mod tests {
|
||||
health_policy_enabled: true,
|
||||
probing_enabled: false,
|
||||
probing_interval_minutes: 10,
|
||||
probe_concurrency: 4,
|
||||
score_top_n: 128,
|
||||
score_fallback_scan_limit: 1024,
|
||||
score_rules: aether_ai_serving::PoolMemberScoreRules::default(),
|
||||
stream_timeout_threshold: 3,
|
||||
stream_timeout_window_seconds: 1800,
|
||||
stream_timeout_cooldown_seconds: 300,
|
||||
|
||||
@@ -24,6 +24,8 @@ mod read_overview;
|
||||
mod read_presets;
|
||||
#[path = "read_routes/resolve_selection.rs"]
|
||||
mod read_resolve_selection;
|
||||
#[path = "read_routes/scores.rs"]
|
||||
mod read_scores;
|
||||
pub(crate) mod selection;
|
||||
mod support;
|
||||
|
||||
@@ -34,11 +36,11 @@ pub(crate) use self::batch_shared::{
|
||||
AdminPoolBatchImportRequest,
|
||||
};
|
||||
pub(crate) use self::support::{
|
||||
admin_pool_provider_id_from_path, parse_admin_pool_key_sort, parse_admin_pool_page,
|
||||
parse_admin_pool_page_size, parse_admin_pool_quick_selectors, parse_admin_pool_search,
|
||||
parse_admin_pool_status_filter, AdminPoolKeySort, AdminPoolKeySortDirection,
|
||||
AdminPoolKeySortField, AdminPoolResolveSelectionRequest,
|
||||
ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
|
||||
admin_pool_provider_id_from_path, admin_pool_provider_id_from_scores_path,
|
||||
parse_admin_pool_key_sort, parse_admin_pool_page, parse_admin_pool_page_size,
|
||||
parse_admin_pool_quick_selectors, parse_admin_pool_search, parse_admin_pool_status_filter,
|
||||
AdminPoolKeySort, AdminPoolKeySortDirection, AdminPoolKeySortField,
|
||||
AdminPoolResolveSelectionRequest, ADMIN_POOL_BANNED_KEY_CLEANUP_EMPTY_MESSAGE,
|
||||
ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
||||
ADMIN_POOL_PROVIDER_CATALOG_WRITER_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
@@ -103,6 +105,11 @@ pub(crate) async fn maybe_build_local_admin_pool_response(
|
||||
read_keys::build_admin_pool_list_keys_response(state, request_context).await?,
|
||||
));
|
||||
}
|
||||
Some("scores") => {
|
||||
return Ok(Some(
|
||||
read_scores::build_admin_pool_scores_response(state, request_context).await?,
|
||||
));
|
||||
}
|
||||
Some("resolve_selection") => {
|
||||
return Ok(Some(
|
||||
read_resolve_selection::build_admin_pool_resolve_selection_response(
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::handlers::admin::shared::{provider_key_status_snapshot_payload, unix_
|
||||
use crate::provider_key_auth::{provider_key_auth_semantics, provider_key_effective_api_formats};
|
||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
||||
use aether_admin::provider::quota as admin_provider_quota_pure;
|
||||
use aether_data_contracts::repository::pool_scores::StoredPoolMemberScore;
|
||||
use aether_data_contracts::repository::provider_catalog::{
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
};
|
||||
@@ -906,6 +907,7 @@ pub(super) fn build_admin_pool_key_payload(
|
||||
key: &StoredProviderCatalogKey,
|
||||
runtime: &AdminProviderPoolRuntimeState,
|
||||
pool_config: Option<AdminProviderPoolConfig>,
|
||||
pool_score: Option<&StoredPoolMemberScore>,
|
||||
codex_cycle_usage_by_code: Option<&BTreeMap<String, StoredProviderApiKeyWindowUsageSummary>>,
|
||||
now_unix_secs: u64,
|
||||
) -> serde_json::Value {
|
||||
@@ -1087,6 +1089,34 @@ pub(super) fn build_admin_pool_key_payload(
|
||||
payload.insert("status_snapshot".to_string(), status_snapshot);
|
||||
payload.insert("quota_updated_at".to_string(), json!(quota_updated_at));
|
||||
payload.insert("health_score".to_string(), json!(health_score));
|
||||
payload.insert(
|
||||
"pool_score".to_string(),
|
||||
pool_score
|
||||
.map(|score| {
|
||||
json!({
|
||||
"id": score.id.clone(),
|
||||
"capability": score.capability.clone(),
|
||||
"scope_kind": score.scope_kind.clone(),
|
||||
"scope_id": score.scope_id.clone(),
|
||||
"score": score.score,
|
||||
"hard_state": score.hard_state.as_database(),
|
||||
"score_version": score.score_version,
|
||||
"score_reason": score.score_reason.clone(),
|
||||
"last_ranked_at": score.last_ranked_at,
|
||||
"last_scheduled_at": score.last_scheduled_at,
|
||||
"last_success_at": score.last_success_at,
|
||||
"last_failure_at": score.last_failure_at,
|
||||
"failure_count": score.failure_count,
|
||||
"last_probe_attempt_at": score.last_probe_attempt_at,
|
||||
"last_probe_success_at": score.last_probe_success_at,
|
||||
"last_probe_failure_at": score.last_probe_failure_at,
|
||||
"probe_failure_count": score.probe_failure_count,
|
||||
"probe_status": score.probe_status.as_database(),
|
||||
"updated_at": score.updated_at,
|
||||
})
|
||||
})
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
payload.insert(
|
||||
"circuit_breaker_open".to_string(),
|
||||
json!(circuit_breaker_open),
|
||||
|
||||
@@ -7,9 +7,13 @@ use super::{
|
||||
AdminPoolKeySortField, AdminProviderPoolRuntimeState, ProviderCatalogKeyListOrder,
|
||||
ProviderCatalogKeyListQuery, ADMIN_POOL_PROVIDER_CATALOG_READER_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use crate::ai_serving::{provider_key_pool_score_id, provider_key_pool_score_scope};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
||||
use aether_data_contracts::repository::pool_scores::{
|
||||
GetPoolMemberScoresByIdsQuery, PoolMemberIdentity, StoredPoolMemberScore,
|
||||
};
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
ProviderApiKeyWindowUsageRequest, StoredProviderApiKeyWindowUsageSummary,
|
||||
@@ -56,6 +60,36 @@ fn admin_pool_current_unix_secs() -> u64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
async fn read_admin_pool_scores_by_key_id(
|
||||
state: &AdminAppState<'_>,
|
||||
provider_id: &str,
|
||||
key_ids: &[String],
|
||||
) -> Result<BTreeMap<String, StoredPoolMemberScore>, GatewayError> {
|
||||
if key_ids.is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let score_scope = provider_key_pool_score_scope();
|
||||
let score_ids = key_ids
|
||||
.iter()
|
||||
.map(|key_id| {
|
||||
let identity =
|
||||
PoolMemberIdentity::provider_api_key(provider_id.to_string(), key_id.clone());
|
||||
provider_key_pool_score_id(&identity, &score_scope)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let scores = state
|
||||
.app()
|
||||
.data
|
||||
.get_pool_member_scores_by_ids(&GetPoolMemberScoresByIdsQuery { ids: score_ids })
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(format!("{err:?}")))?;
|
||||
Ok(scores
|
||||
.into_iter()
|
||||
.map(|score| (score.member_id.clone(), score))
|
||||
.collect::<BTreeMap<_, _>>())
|
||||
}
|
||||
|
||||
fn admin_pool_codex_cycle_usage_request(
|
||||
key: &StoredProviderCatalogKey,
|
||||
window: &serde_json::Map<String, serde_json::Value>,
|
||||
@@ -379,6 +413,9 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
||||
};
|
||||
|
||||
let key_ids = keys.iter().map(|key| key.id.clone()).collect::<Vec<_>>();
|
||||
let pool_scores_by_key_id = read_admin_pool_scores_by_key_id(state, &provider.id, &key_ids)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
@@ -414,6 +451,7 @@ pub(super) async fn build_admin_pool_list_keys_response(
|
||||
&key,
|
||||
&runtime,
|
||||
pool_config.clone(),
|
||||
pool_scores_by_key_id.get(&key.id),
|
||||
codex_cycle_usage_by_key.get(&key.id),
|
||||
now_unix_secs,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::{
|
||||
admin_pool_provider_id_from_scores_path, build_admin_pool_error_response,
|
||||
parse_admin_pool_page, parse_admin_pool_page_size,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::pool_scores::{
|
||||
ListPoolMemberScoresQuery, PoolMemberHardState, PoolMemberProbeStatus,
|
||||
POOL_KIND_PROVIDER_KEY_POOL, POOL_SCORE_CAPABILITY_ACCOUNT, POOL_SCORE_SCOPE_KIND_ACCOUNT,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) async fn build_admin_pool_scores_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(provider_id) = admin_pool_provider_id_from_scores_path(request_context.path()) else {
|
||||
return Ok(build_admin_pool_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"provider_id 无效",
|
||||
));
|
||||
};
|
||||
|
||||
let query = request_context.query_string();
|
||||
let page = match parse_admin_pool_page(query) {
|
||||
Ok(value) => value,
|
||||
Err(message) => {
|
||||
return Ok(build_admin_pool_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
));
|
||||
}
|
||||
};
|
||||
let page_size = match parse_admin_pool_page_size(query) {
|
||||
Ok(value) => value.min(500),
|
||||
Err(message) => {
|
||||
return Ok(build_admin_pool_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
));
|
||||
}
|
||||
};
|
||||
let offset = page.saturating_sub(1).saturating_mul(page_size);
|
||||
let hard_states = match parse_hard_state_filter(query) {
|
||||
Ok(value) => value,
|
||||
Err(message) => {
|
||||
return Ok(build_admin_pool_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
));
|
||||
}
|
||||
};
|
||||
let probe_statuses = match parse_probe_status_filter(query) {
|
||||
Ok(value) => value,
|
||||
Err(message) => {
|
||||
return Ok(build_admin_pool_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
message,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let scores = state
|
||||
.app()
|
||||
.data
|
||||
.list_pool_member_scores(&ListPoolMemberScoresQuery {
|
||||
pool_kind: POOL_KIND_PROVIDER_KEY_POOL.to_string(),
|
||||
pool_id: provider_id.clone(),
|
||||
capability: Some(POOL_SCORE_CAPABILITY_ACCOUNT.to_string()),
|
||||
scope_kind: Some(POOL_SCORE_SCOPE_KIND_ACCOUNT.to_string()),
|
||||
scope_id: None,
|
||||
hard_states,
|
||||
probe_statuses,
|
||||
offset,
|
||||
limit: page_size,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(format!("{err:?}")))?;
|
||||
|
||||
let key_ids = scores
|
||||
.iter()
|
||||
.map(|score| score.member_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let keys = state
|
||||
.app()
|
||||
.read_provider_catalog_keys_by_ids(&key_ids)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|key| (key.id.clone(), key))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let items = scores
|
||||
.into_iter()
|
||||
.map(|score| {
|
||||
let key = keys.get(&score.member_id);
|
||||
json!({
|
||||
"id": score.id,
|
||||
"pool_kind": score.pool_kind,
|
||||
"pool_id": score.pool_id,
|
||||
"member_kind": score.member_kind,
|
||||
"member_id": score.member_id,
|
||||
"capability": score.capability,
|
||||
"scope_kind": score.scope_kind,
|
||||
"scope_id": score.scope_id,
|
||||
"score": score.score,
|
||||
"hard_state": score.hard_state.as_database(),
|
||||
"score_version": score.score_version,
|
||||
"score_reason": score.score_reason,
|
||||
"last_ranked_at": score.last_ranked_at,
|
||||
"last_scheduled_at": score.last_scheduled_at,
|
||||
"last_success_at": score.last_success_at,
|
||||
"last_failure_at": score.last_failure_at,
|
||||
"failure_count": score.failure_count,
|
||||
"last_probe_attempt_at": score.last_probe_attempt_at,
|
||||
"last_probe_success_at": score.last_probe_success_at,
|
||||
"last_probe_failure_at": score.last_probe_failure_at,
|
||||
"probe_failure_count": score.probe_failure_count,
|
||||
"probe_status": score.probe_status.as_database(),
|
||||
"updated_at": score.updated_at,
|
||||
"key": key.map(|key| json!({
|
||||
"id": key.id,
|
||||
"name": key.name,
|
||||
"auth_type": key.auth_type,
|
||||
"is_active": key.is_active,
|
||||
"internal_priority": key.internal_priority,
|
||||
"last_used_at": key.last_used_at_unix_secs,
|
||||
}))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(json!({
|
||||
"provider_id": provider_id,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"filters": {
|
||||
"api_format": serde_json::Value::Null,
|
||||
"model_id": serde_json::Value::Null,
|
||||
"hard_state": query_param_value(query, "hard_state"),
|
||||
"probe_status": query_param_value(query, "probe_status")
|
||||
},
|
||||
"items": items
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
fn parse_hard_state_filter(query: Option<&str>) -> Result<Vec<PoolMemberHardState>, String> {
|
||||
let Some(raw) = query_param_value(query, "hard_state") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
raw.split(',')
|
||||
.map(|value| match value.trim() {
|
||||
"available" => Ok(PoolMemberHardState::Available),
|
||||
"unknown" => Ok(PoolMemberHardState::Unknown),
|
||||
"cooldown" => Ok(PoolMemberHardState::Cooldown),
|
||||
"quota_exhausted" => Ok(PoolMemberHardState::QuotaExhausted),
|
||||
"auth_invalid" => Ok(PoolMemberHardState::AuthInvalid),
|
||||
"banned" => Ok(PoolMemberHardState::Banned),
|
||||
"inactive" => Ok(PoolMemberHardState::Inactive),
|
||||
_ => Err("hard_state must be one of: available, unknown, cooldown, quota_exhausted, auth_invalid, banned, inactive".to_string()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_probe_status_filter(
|
||||
query: Option<&str>,
|
||||
) -> Result<Option<Vec<PoolMemberProbeStatus>>, String> {
|
||||
let Some(raw) = query_param_value(query, "probe_status") else {
|
||||
return Ok(None);
|
||||
};
|
||||
raw.split(',')
|
||||
.map(|value| match value.trim() {
|
||||
"never" => Ok(PoolMemberProbeStatus::Never),
|
||||
"ok" => Ok(PoolMemberProbeStatus::Ok),
|
||||
"failed" => Ok(PoolMemberProbeStatus::Failed),
|
||||
"stale" => Ok(PoolMemberProbeStatus::Stale),
|
||||
"in_progress" => Ok(PoolMemberProbeStatus::InProgress),
|
||||
_ => Err(
|
||||
"probe_status must be one of: never, ok, failed, stale, in_progress".to_string(),
|
||||
),
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Some)
|
||||
}
|
||||
@@ -149,6 +149,18 @@ pub(crate) fn admin_pool_provider_id_from_path(request_path: &str) -> Option<Str
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn admin_pool_provider_id_from_scores_path(request_path: &str) -> Option<String> {
|
||||
let raw = request_path.strip_prefix("/api/admin/pool/")?;
|
||||
let mut segments = raw.split('/');
|
||||
let provider_id = segments.next()?.trim();
|
||||
let scores_segment = segments.next()?.trim_end_matches('/').trim();
|
||||
if provider_id.is_empty() || scores_segment != "scores" {
|
||||
None
|
||||
} else {
|
||||
Some(provider_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_admin_pool_route(request_context: &AdminRequestContext<'_>) -> bool {
|
||||
let normalized_path = request_context.path().trim_end_matches('/');
|
||||
let path = if normalized_path.is_empty() {
|
||||
@@ -164,6 +176,10 @@ pub(crate) fn is_admin_pool_route(request_context: &AdminRequestContext<'_>) ->
|
||||
&& path.starts_with("/api/admin/pool/")
|
||||
&& path.ends_with("/keys")
|
||||
&& path.matches('/').count() == 5)
|
||||
|| (request_context.method() == http::Method::GET
|
||||
&& path.starts_with("/api/admin/pool/")
|
||||
&& path.ends_with("/scores")
|
||||
&& path.matches('/').count() == 5)
|
||||
|| (request_context.method() == http::Method::POST
|
||||
&& path.starts_with("/api/admin/pool/")
|
||||
&& path.ends_with("/keys/batch-import")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::LocalProviderDeleteTaskState;
|
||||
use aether_ai_serving::PoolMemberScoreRules;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -39,6 +40,10 @@ pub(crate) struct AdminProviderPoolConfig {
|
||||
pub(crate) health_policy_enabled: bool,
|
||||
pub(crate) probing_enabled: bool,
|
||||
pub(crate) probing_interval_minutes: u64,
|
||||
pub(crate) probe_concurrency: u64,
|
||||
pub(crate) score_top_n: u64,
|
||||
pub(crate) score_fallback_scan_limit: u64,
|
||||
pub(crate) score_rules: PoolMemberScoreRules,
|
||||
pub(crate) stream_timeout_threshold: u64,
|
||||
pub(crate) stream_timeout_window_seconds: u64,
|
||||
pub(crate) stream_timeout_cooldown_seconds: u64,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::{AdminAppState, AdminRequestContext};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::Response;
|
||||
use axum::http::{HeaderMap, Response};
|
||||
|
||||
pub(crate) enum AdminCancelVideoTaskError {
|
||||
NotFound,
|
||||
@@ -14,6 +14,7 @@ pub(crate) enum AdminCancelVideoTaskError {
|
||||
pub(crate) struct AdminRouteRequest<'a> {
|
||||
state: AdminAppState<'a>,
|
||||
request_context: AdminRequestContext<'a>,
|
||||
request_headers: &'a HeaderMap,
|
||||
request_body: Option<&'a Bytes>,
|
||||
}
|
||||
|
||||
@@ -21,11 +22,13 @@ impl<'a> AdminRouteRequest<'a> {
|
||||
pub(crate) fn new(
|
||||
state: &'a AppState,
|
||||
request_context: &'a crate::control::GatewayPublicRequestContext,
|
||||
request_headers: &'a HeaderMap,
|
||||
request_body: Option<&'a Bytes>,
|
||||
) -> Self {
|
||||
Self {
|
||||
state: AdminAppState::new(state),
|
||||
request_context: AdminRequestContext::new(request_context),
|
||||
request_headers,
|
||||
request_body,
|
||||
}
|
||||
}
|
||||
@@ -38,6 +41,10 @@ impl<'a> AdminRouteRequest<'a> {
|
||||
self.request_context
|
||||
}
|
||||
|
||||
pub(crate) fn request_headers(self) -> &'a HeaderMap {
|
||||
self.request_headers
|
||||
}
|
||||
|
||||
pub(crate) fn request_body(self) -> Option<&'a Bytes> {
|
||||
self.request_body
|
||||
}
|
||||
|
||||
@@ -326,14 +326,14 @@ async fn refresh_imported_oauth_key_after_persist(
|
||||
provider: &aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider,
|
||||
key_id: &str,
|
||||
) -> Result<(), GatewayError> {
|
||||
let endpoints = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider.id))
|
||||
.await?;
|
||||
let Some(endpoint) =
|
||||
crate::handlers::admin::provider::oauth::runtime::provider_oauth_runtime_endpoint_for_provider(
|
||||
crate::handlers::admin::provider::oauth::runtime::resolve_provider_oauth_runtime_endpoints(
|
||||
state,
|
||||
provider,
|
||||
provider.provider_type.as_str(),
|
||||
&endpoints,
|
||||
)
|
||||
.await?
|
||||
.runtime_endpoint
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::control::{
|
||||
management_token_permission_catalog_payload, normalize_assignable_management_token_permissions,
|
||||
management_token_permission_catalog_payload,
|
||||
management_token_permissions_cover_all_assignable_permissions,
|
||||
normalize_assignable_management_token_permissions,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
|
||||
@@ -317,12 +319,17 @@ pub(crate) async fn maybe_build_local_admin_management_tokens_response(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if decision
|
||||
let is_management_token = decision
|
||||
.admin_principal
|
||||
.as_ref()
|
||||
.and_then(|principal| principal.management_token_id.as_deref())
|
||||
.is_some()
|
||||
{
|
||||
.is_some();
|
||||
let management_token_is_full = decision
|
||||
.admin_principal
|
||||
.as_ref()
|
||||
.and_then(|principal| principal.management_token_permissions.as_deref())
|
||||
.is_none_or(management_token_permissions_cover_all_assignable_permissions);
|
||||
if is_management_token && !management_token_is_full {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub(super) async fn maybe_build_local_internal_proxy_response(
|
||||
pub(super) async fn maybe_build_local_admin_proxy_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
request_headers: &http::HeaderMap,
|
||||
request_body: Option<&Bytes>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(decision) = request_context.control_decision.as_ref() else {
|
||||
@@ -49,6 +50,7 @@ pub(super) async fn maybe_build_local_admin_proxy_response(
|
||||
admin_api::maybe_build_local_admin_response(admin_api::AdminRouteRequest::new(
|
||||
state,
|
||||
request_context,
|
||||
request_headers,
|
||||
request_body,
|
||||
))
|
||||
.await
|
||||
|
||||
@@ -874,9 +874,13 @@ pub(crate) async fn proxy_request(
|
||||
request_permit.take(),
|
||||
));
|
||||
}
|
||||
if let Some(response) =
|
||||
maybe_build_local_admin_proxy_response(&state, &request_context, local_proxy_body.as_ref())
|
||||
.await?
|
||||
if let Some(response) = maybe_build_local_admin_proxy_response(
|
||||
&state,
|
||||
&request_context,
|
||||
&parts.headers,
|
||||
local_proxy_body.as_ref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let execution_path =
|
||||
resolve_local_proxy_execution_path(&response, EXECUTION_PATH_PUBLIC_PROXY_PASSTHROUGH);
|
||||
|
||||
@@ -20,6 +20,7 @@ pub(crate) use self::system_modules_helpers::{
|
||||
};
|
||||
|
||||
pub(crate) use self::support::{
|
||||
build_unhandled_public_support_response, matches_model_mapping_for_models,
|
||||
maybe_build_local_admin_announcements_response, maybe_build_local_public_support_response,
|
||||
build_api_key_install_session_response, build_unhandled_public_support_response,
|
||||
matches_model_mapping_for_models, maybe_build_local_admin_announcements_response,
|
||||
maybe_build_local_public_support_response, CreateApiKeyInstallSessionRequest,
|
||||
};
|
||||
|
||||
@@ -66,6 +66,9 @@ use self::support_auth::{
|
||||
};
|
||||
use self::support_billing::maybe_build_local_billing_response;
|
||||
use self::support_dashboard::maybe_build_local_dashboard_response;
|
||||
pub(crate) use self::support_install::{
|
||||
build_api_key_install_session_response, CreateApiKeyInstallSessionRequest,
|
||||
};
|
||||
use self::support_install::{
|
||||
handle_users_me_api_key_install_session_create, maybe_build_local_install_response,
|
||||
users_me_api_key_install_sessions_path_matches,
|
||||
|
||||
@@ -17,7 +17,7 @@ const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum InstallTargetCli {
|
||||
pub(crate) enum InstallTargetCli {
|
||||
ClaudeCode,
|
||||
CodexCli,
|
||||
GeminiCli,
|
||||
@@ -25,7 +25,7 @@ enum InstallTargetCli {
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum InstallTargetSystem {
|
||||
pub(crate) enum InstallTargetSystem {
|
||||
Macos,
|
||||
Linux,
|
||||
Windows,
|
||||
@@ -33,9 +33,9 @@ enum InstallTargetSystem {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UsersMeCreateInstallSessionRequest {
|
||||
target_cli: InstallTargetCli,
|
||||
target_system: InstallTargetSystem,
|
||||
pub(crate) struct CreateApiKeyInstallSessionRequest {
|
||||
pub(crate) target_cli: InstallTargetCli,
|
||||
pub(crate) target_system: InstallTargetSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -240,20 +240,60 @@ PY
|
||||
;;
|
||||
codex_cli)
|
||||
mkdir -p "$HOME/.codex"
|
||||
cat > "$HOME/.codex/auth.json" <<EOF
|
||||
{{"OPENAI_API_KEY":"$AETHER_API_KEY"}}
|
||||
EOF
|
||||
cat > "$HOME/.codex/config.toml" <<EOF
|
||||
# Managed by Aether
|
||||
model_provider = "aether"
|
||||
python3 - "$HOME/.codex/config.toml" "$AETHER_BASE_URL" "$AETHER_API_KEY" <<'PY'
|
||||
import pathlib, re, sys
|
||||
|
||||
[model_providers.aether]
|
||||
name = "Aether"
|
||||
base_url = "$AETHER_BASE_URL/v1"
|
||||
env_key = "OPENAI_API_KEY"
|
||||
wire_api = "chat"
|
||||
EOF
|
||||
chmod 600 "$HOME/.codex/auth.json" "$HOME/.codex/config.toml" 2>/dev/null || true
|
||||
path = pathlib.Path(sys.argv[1])
|
||||
base_url = sys.argv[2].rstrip('/') + '/v1'
|
||||
api_key = sys.argv[3]
|
||||
text = path.read_text() if path.exists() else ''
|
||||
lines = text.splitlines()
|
||||
|
||||
def quote_toml(value: str) -> str:
|
||||
return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
||||
|
||||
result = []
|
||||
in_aether = False
|
||||
top_model_provider_set = False
|
||||
seen_section = False
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if re.match(r'^\[.*\]$', stripped):
|
||||
seen_section = True
|
||||
in_aether = stripped == '[model_providers.aether]'
|
||||
if in_aether:
|
||||
continue
|
||||
if in_aether:
|
||||
continue
|
||||
if not seen_section and re.match(r'^model_provider\s*=', stripped):
|
||||
if not top_model_provider_set:
|
||||
result.append('model_provider = "aether"')
|
||||
top_model_provider_set = True
|
||||
continue
|
||||
result.append(line)
|
||||
|
||||
if not top_model_provider_set:
|
||||
insert_at = next((idx for idx, line in enumerate(result) if line.strip().startswith('[')), len(result))
|
||||
while insert_at > 0 and result[insert_at - 1].strip() == '':
|
||||
insert_at -= 1
|
||||
result[insert_at:insert_at] = ['model_provider = "aether"', '']
|
||||
|
||||
while result and result[-1].strip() == '':
|
||||
result.pop()
|
||||
if result:
|
||||
result.append('')
|
||||
result.extend([
|
||||
'# Managed by Aether',
|
||||
'[model_providers.aether]',
|
||||
'name = "Aether"',
|
||||
f'base_url = {{quote_toml(base_url)}}',
|
||||
'wire_api = "responses"',
|
||||
'requires_openai_auth = false',
|
||||
f'experimental_bearer_token = {{quote_toml(api_key)}}',
|
||||
])
|
||||
path.write_text('\n'.join(result) + '\n')
|
||||
PY
|
||||
chmod 600 "$HOME/.codex/config.toml" 2>/dev/null || true
|
||||
;;
|
||||
gemini_cli)
|
||||
mkdir -p "$HOME/.gemini"
|
||||
@@ -329,8 +369,51 @@ if ($TargetCli -eq 'claude_code') {{
|
||||
$Data | ConvertTo-Json -Depth 8 | Set-Content $Path -Encoding UTF8
|
||||
}} elseif ($TargetCli -eq 'codex_cli') {{
|
||||
$Dir = Join-Path $HomeDir '.codex'; New-Item -ItemType Directory -Force -Path $Dir | Out-Null
|
||||
Set-Content (Join-Path $Dir 'auth.json') -Value (@{{ OPENAI_API_KEY = $AetherApiKey }} | ConvertTo-Json) -Encoding UTF8
|
||||
Set-Content (Join-Path $Dir 'config.toml') -Value "# Managed by Aether`nmodel_provider = \"aether\"`n`n[model_providers.aether]`nname = \"Aether\"`nbase_url = \"$AetherBaseUrl/v1\"`nenv_key = \"OPENAI_API_KEY\"`nwire_api = \"chat\"`n" -Encoding UTF8
|
||||
$Path = Join-Path $Dir 'config.toml'
|
||||
$Text = if (Test-Path $Path) {{ Get-Content $Path -Raw }} else {{ '' }}
|
||||
$Lines = if ($Text.Length -gt 0) {{ $Text -split "`r?`n" }} else {{ @() }}
|
||||
$Result = New-Object System.Collections.Generic.List[string]
|
||||
$InAether = $false
|
||||
$TopModelProviderSet = $false
|
||||
$SeenSection = $false
|
||||
foreach ($Line in $Lines) {{
|
||||
$Stripped = $Line.Trim()
|
||||
if ($Stripped -match '^\[.*\]$') {{
|
||||
$SeenSection = $true
|
||||
$InAether = $Stripped -eq '[model_providers.aether]'
|
||||
if ($InAether) {{ continue }}
|
||||
}}
|
||||
if ($InAether) {{ continue }}
|
||||
if (-not $SeenSection -and $Stripped -match '^model_provider\s*=') {{
|
||||
if (-not $TopModelProviderSet) {{
|
||||
$Result.Add('model_provider = "aether"')
|
||||
$TopModelProviderSet = $true
|
||||
}}
|
||||
continue
|
||||
}}
|
||||
$Result.Add($Line)
|
||||
}}
|
||||
if (-not $TopModelProviderSet) {{
|
||||
$InsertAt = $Result.Count
|
||||
for ($Index = 0; $Index -lt $Result.Count; $Index++) {{
|
||||
if ($Result[$Index].Trim().StartsWith('[')) {{ $InsertAt = $Index; break }}
|
||||
}}
|
||||
while ($InsertAt -gt 0 -and $Result[$InsertAt - 1].Trim() -eq '') {{ $InsertAt-- }}
|
||||
$Result.Insert($InsertAt, '')
|
||||
$Result.Insert($InsertAt, 'model_provider = "aether"')
|
||||
}}
|
||||
while ($Result.Count -gt 0 -and $Result[$Result.Count - 1].Trim() -eq '') {{ $Result.RemoveAt($Result.Count - 1) }}
|
||||
if ($Result.Count -gt 0) {{ $Result.Add('') }}
|
||||
$EscapedBaseUrl = ($AetherBaseUrl.TrimEnd('/') + '/v1').Replace('\', '\\').Replace('"', '\"')
|
||||
$EscapedApiKey = $AetherApiKey.Replace('\', '\\').Replace('"', '\"')
|
||||
$Result.Add('# Managed by Aether')
|
||||
$Result.Add('[model_providers.aether]')
|
||||
$Result.Add('name = "Aether"')
|
||||
$Result.Add("base_url = `"$EscapedBaseUrl`"")
|
||||
$Result.Add('wire_api = "responses"')
|
||||
$Result.Add('requires_openai_auth = false')
|
||||
$Result.Add("experimental_bearer_token = `"$EscapedApiKey`"")
|
||||
Set-Content -Path $Path -Value (($Result -join "`n") + "`n") -Encoding UTF8
|
||||
}} elseif ($TargetCli -eq 'gemini_cli') {{
|
||||
$Dir = Join-Path $HomeDir '.gemini'; New-Item -ItemType Directory -Force -Path $Dir | Out-Null
|
||||
Set-Content (Join-Path $Dir '.env') -Value "GEMINI_API_KEY=$AetherApiKey`nGOOGLE_API_KEY=$AetherApiKey`nGOOGLE_GEMINI_BASE_URL=$AetherBaseUrl`nAETHER_BASE_URL=$AetherBaseUrl`n" -Encoding UTF8
|
||||
@@ -375,7 +458,7 @@ pub(super) async fn handle_users_me_api_key_install_session_create(
|
||||
let Some(request_body) = request_body else {
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, "请求数据验证失败", false);
|
||||
};
|
||||
let payload = match serde_json::from_slice::<UsersMeCreateInstallSessionRequest>(request_body) {
|
||||
let payload = match serde_json::from_slice::<CreateApiKeyInstallSessionRequest>(request_body) {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return build_auth_error_response(
|
||||
@@ -426,11 +509,32 @@ pub(super) async fn handle_users_me_api_key_install_session_create(
|
||||
);
|
||||
};
|
||||
|
||||
build_api_key_install_session_response(
|
||||
state,
|
||||
request_context,
|
||||
headers,
|
||||
record.api_key_id.clone(),
|
||||
record.name.unwrap_or_else(|| "API Key".to_string()),
|
||||
api_key,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn build_api_key_install_session_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
api_key_id: String,
|
||||
api_key_name: String,
|
||||
api_key: String,
|
||||
payload: CreateApiKeyInstallSessionRequest,
|
||||
) -> Response<Body> {
|
||||
let code = generate_install_code();
|
||||
let expires_at_unix_secs = unix_secs_now().saturating_add(INSTALL_SESSION_TTL_SECS);
|
||||
let session = StoredInstallSession {
|
||||
api_key_id: record.api_key_id.clone(),
|
||||
api_key_name: record.name.unwrap_or_else(|| "API Key".to_string()),
|
||||
api_key_id,
|
||||
api_key_name,
|
||||
api_key,
|
||||
base_url: base_url_from_request(headers, request_context),
|
||||
target_cli: payload.target_cli,
|
||||
@@ -559,3 +663,49 @@ pub(super) async fn maybe_build_local_install_response(
|
||||
);
|
||||
Some(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_session(target_cli: InstallTargetCli) -> StoredInstallSession {
|
||||
StoredInstallSession {
|
||||
api_key_id: "key-1".to_string(),
|
||||
api_key_name: "Key 1".to_string(),
|
||||
api_key: "sk-test".to_string(),
|
||||
base_url: "http://localhost:8084".to_string(),
|
||||
target_cli,
|
||||
target_system: InstallTargetSystem::Linux,
|
||||
expires_at_unix_secs: u64::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_unix_script_preserves_config_and_uses_responses_bearer_token() {
|
||||
let script = build_unix_script(&test_session(InstallTargetCli::CodexCli));
|
||||
|
||||
assert!(script.contains("path.read_text() if path.exists() else ''"));
|
||||
assert!(script.contains("stripped == '[model_providers.aether]'"));
|
||||
assert!(script.contains("model_provider = \"aether\""));
|
||||
assert!(script.contains("wire_api = \"responses\""));
|
||||
assert!(script.contains("requires_openai_auth = false"));
|
||||
assert!(script.contains("experimental_bearer_token ="));
|
||||
assert!(!script.contains("wire_api = \"chat\""));
|
||||
assert!(!script.contains("cat > \"$HOME/.codex/config.toml\""));
|
||||
assert!(!script.contains("auth.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_powershell_script_preserves_config_and_uses_responses_bearer_token() {
|
||||
let script = build_powershell_script(&test_session(InstallTargetCli::CodexCli));
|
||||
|
||||
assert!(script.contains("Get-Content $Path -Raw"));
|
||||
assert!(script.contains("$Stripped -eq '[model_providers.aether]'"));
|
||||
assert!(script.contains("model_provider = \"aether\""));
|
||||
assert!(script.contains("wire_api = \"responses\""));
|
||||
assert!(script.contains("requires_openai_auth = false"));
|
||||
assert!(script.contains("experimental_bearer_token ="));
|
||||
assert!(!script.contains("wire_api = \"chat\""));
|
||||
assert!(!script.contains("auth.json"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ pub(crate) use super::super::admin::provider::pool::config::{
|
||||
admin_provider_pool_cache_affinity_enabled, admin_provider_pool_config_from_config_value,
|
||||
};
|
||||
pub(crate) use super::super::admin::provider::pool::runtime::{
|
||||
admin_provider_pool_key_circuit_breaker_reason, read_admin_provider_pool_runtime_state,
|
||||
record_admin_provider_pool_error, record_admin_provider_pool_stream_timeout,
|
||||
record_admin_provider_pool_success, release_admin_provider_pool_key_lease,
|
||||
try_claim_admin_provider_pool_key, ADMIN_PROVIDER_POOL_KEY_LEASE_TTL_MS,
|
||||
admin_provider_pool_key_circuit_breaker_reason, read_admin_provider_pool_key_cooldown_reason,
|
||||
read_admin_provider_pool_runtime_state, record_admin_provider_pool_error,
|
||||
record_admin_provider_pool_stream_timeout, record_admin_provider_pool_success,
|
||||
release_admin_provider_pool_key_lease,
|
||||
};
|
||||
pub(crate) use super::super::admin::provider::shared::support::{
|
||||
AdminProviderPoolConfig, AdminProviderPoolRuntimeState, AdminProviderPoolSchedulingPreset,
|
||||
|
||||
@@ -311,7 +311,11 @@ pub(crate) fn admin_proxy_local_requires_buffered_body(
|
||||
| (Some("payments_manage"), http::Method::POST, Some("credit_order"))
|
||||
| (Some("payments_manage"), http::Method::POST, Some("create_redeem_code_batch"))
|
||||
| (Some("payments_manage"), http::Method::POST, Some("delete_redeem_code_batch"))
|
||||
| (Some("api_keys_manage"), http::Method::POST, Some("create_api_key"))
|
||||
| (
|
||||
Some("api_keys_manage"),
|
||||
http::Method::POST,
|
||||
Some("create_api_key" | "create_api_key_install_session"),
|
||||
)
|
||||
| (Some("api_keys_manage"), http::Method::PUT, Some("update_api_key"))
|
||||
| (Some("api_keys_manage"), http::Method::PATCH, Some("toggle_api_key"))
|
||||
| (Some("adaptive_manage"), http::Method::PATCH, Some("toggle_mode"))
|
||||
|
||||
Reference in New Issue
Block a user