mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 09:57:47 +08:00
Merge remote-tracking branch 'origin/main' into codex/pool-key-bulk-management-20260714
# Conflicts: # apps/aether-gateway/src/handlers/admin/request/provider/tasks.rs # frontend/src/api/endpoints/pool.ts
This commit is contained in:
@@ -12,10 +12,7 @@ use super::{
|
||||
StoredWalletSnapshot, UpdateManagementTokenRecord, UpsertOAuthProviderConfigRecord,
|
||||
};
|
||||
use crate::LocalMutationOutcome;
|
||||
use aether_data::repository::auth::{
|
||||
read_resolved_auth_api_key_snapshot_by_key_hash,
|
||||
read_resolved_auth_api_key_snapshot_by_user_api_key_ids,
|
||||
};
|
||||
use aether_data::repository::auth::ResolvedAuthApiKeySnapshotReader;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct GatewayUserEffectiveListPolicies {
|
||||
@@ -1751,15 +1748,14 @@ impl GatewayDataState {
|
||||
let snapshot = crate::request_diagnostics::observe_db_operation(
|
||||
"auth_api_key_snapshot",
|
||||
self.database_pool_summary(),
|
||||
read_resolved_auth_api_key_snapshot_by_user_api_key_ids(
|
||||
self,
|
||||
self.find_stored_auth_api_key_snapshot(AuthApiKeyLookupKey::UserApiKeyIds {
|
||||
user_id,
|
||||
api_key_id,
|
||||
now_unix_secs,
|
||||
),
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
self.apply_user_group_effective_policies(snapshot).await
|
||||
self.apply_user_group_effective_policies(snapshot, now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn read_auth_api_key_snapshot_by_key_hash(
|
||||
@@ -1770,25 +1766,33 @@ impl GatewayDataState {
|
||||
let snapshot = crate::request_diagnostics::observe_db_operation(
|
||||
"auth_api_key_snapshot_by_hash",
|
||||
self.database_pool_summary(),
|
||||
read_resolved_auth_api_key_snapshot_by_key_hash(self, key_hash, now_unix_secs),
|
||||
self.find_stored_auth_api_key_snapshot(AuthApiKeyLookupKey::KeyHash(key_hash)),
|
||||
)
|
||||
.await?;
|
||||
self.apply_user_group_effective_policies(snapshot).await
|
||||
self.apply_user_group_effective_policies(snapshot, now_unix_secs)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn apply_user_group_effective_policies(
|
||||
&self,
|
||||
snapshot: Option<GatewayAuthApiKeySnapshot>,
|
||||
snapshot: Option<StoredAuthApiKeySnapshot>,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<Option<GatewayAuthApiKeySnapshot>, DataLayerError> {
|
||||
let Some(mut snapshot) = snapshot else {
|
||||
return Ok(None);
|
||||
};
|
||||
if snapshot.user_role.eq_ignore_ascii_case("admin") && !snapshot.api_key_is_standalone {
|
||||
apply_admin_unrestricted_auth_snapshot(&mut snapshot);
|
||||
return Ok(Some(snapshot));
|
||||
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
|
||||
snapshot,
|
||||
now_unix_secs,
|
||||
)));
|
||||
}
|
||||
let Some(repository) = self.user_reader.as_ref() else {
|
||||
return Ok(Some(snapshot));
|
||||
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
|
||||
snapshot,
|
||||
now_unix_secs,
|
||||
)));
|
||||
};
|
||||
let Some(user) = crate::request_diagnostics::observe_db_operation(
|
||||
"auth_user_policy",
|
||||
@@ -1797,57 +1801,37 @@ impl GatewayDataState {
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(Some(snapshot));
|
||||
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
|
||||
snapshot,
|
||||
now_unix_secs,
|
||||
)));
|
||||
};
|
||||
if user.role.eq_ignore_ascii_case("admin") && !snapshot.api_key_is_standalone {
|
||||
snapshot.user_role = user.role;
|
||||
apply_admin_unrestricted_auth_snapshot(&mut snapshot);
|
||||
return Ok(Some(snapshot));
|
||||
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
|
||||
snapshot,
|
||||
now_unix_secs,
|
||||
)));
|
||||
}
|
||||
let groups = self
|
||||
.effective_user_groups_for_user(&snapshot.user_id)
|
||||
.await?;
|
||||
|
||||
let mut allowed_providers =
|
||||
resolve_effective_list_policy(None, "unrestricted", &groups, |group| {
|
||||
(
|
||||
&group.allowed_providers_mode,
|
||||
group.allowed_providers.clone(),
|
||||
)
|
||||
});
|
||||
let mut allowed_api_formats =
|
||||
resolve_effective_list_policy(None, "unrestricted", &groups, |group| {
|
||||
(
|
||||
&group.allowed_api_formats_mode,
|
||||
group.allowed_api_formats.clone(),
|
||||
)
|
||||
});
|
||||
let mut allowed_models =
|
||||
resolve_effective_list_policy(None, "unrestricted", &groups, |group| {
|
||||
(&group.allowed_models_mode, group.allowed_models.clone())
|
||||
});
|
||||
let user_rate_limit = resolve_effective_rate_limit_policy(None, "system", &groups);
|
||||
if !snapshot.api_key_is_standalone {
|
||||
constrain_api_key_list_policy_to_user_policy(
|
||||
&mut allowed_providers,
|
||||
&mut snapshot.api_key_allowed_providers,
|
||||
);
|
||||
constrain_api_key_list_policy_to_user_policy(
|
||||
&mut allowed_api_formats,
|
||||
&mut snapshot.api_key_allowed_api_formats,
|
||||
);
|
||||
constrain_api_key_list_policy_to_user_policy(
|
||||
&mut allowed_models,
|
||||
&mut snapshot.api_key_allowed_models,
|
||||
);
|
||||
}
|
||||
snapshot.apply_user_policy(
|
||||
let GatewayUserEffectiveListPolicies {
|
||||
allowed_providers,
|
||||
allowed_api_formats,
|
||||
allowed_models,
|
||||
user_rate_limit,
|
||||
);
|
||||
Ok(Some(snapshot))
|
||||
} = resolve_group_effective_list_policies(&groups);
|
||||
let user_rate_limit = resolve_effective_rate_limit_policy(None, "system", &groups);
|
||||
snapshot.user_allowed_providers = allowed_providers;
|
||||
snapshot.user_allowed_api_formats = allowed_api_formats;
|
||||
snapshot.user_allowed_models = allowed_models;
|
||||
snapshot.user_rate_limit = user_rate_limit;
|
||||
Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
|
||||
snapshot,
|
||||
now_unix_secs,
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_user_effective_list_policies(
|
||||
@@ -1863,36 +1847,7 @@ impl GatewayDataState {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
Ok(GatewayUserEffectiveListPolicies {
|
||||
allowed_providers: resolve_effective_list_policy(
|
||||
user.allowed_providers.clone(),
|
||||
&user.allowed_providers_mode,
|
||||
&groups,
|
||||
|group| {
|
||||
(
|
||||
&group.allowed_providers_mode,
|
||||
group.allowed_providers.clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
allowed_api_formats: resolve_effective_list_policy(
|
||||
user.allowed_api_formats.clone(),
|
||||
&user.allowed_api_formats_mode,
|
||||
&groups,
|
||||
|group| {
|
||||
(
|
||||
&group.allowed_api_formats_mode,
|
||||
group.allowed_api_formats.clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
allowed_models: resolve_effective_list_policy(
|
||||
user.allowed_models.clone(),
|
||||
&user.allowed_models_mode,
|
||||
&groups,
|
||||
|group| (&group.allowed_models_mode, group.allowed_models.clone()),
|
||||
),
|
||||
})
|
||||
Ok(resolve_group_effective_list_policies(&groups))
|
||||
}
|
||||
|
||||
async fn effective_user_groups_for_user(
|
||||
@@ -1970,7 +1925,7 @@ impl GatewayDataState {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_admin_unrestricted_auth_snapshot(snapshot: &mut GatewayAuthApiKeySnapshot) {
|
||||
fn apply_admin_unrestricted_auth_snapshot(snapshot: &mut StoredAuthApiKeySnapshot) {
|
||||
snapshot.user_allowed_providers = None;
|
||||
snapshot.user_allowed_api_formats = None;
|
||||
snapshot.user_allowed_models = None;
|
||||
@@ -1982,6 +1937,35 @@ fn apply_admin_unrestricted_auth_snapshot(snapshot: &mut GatewayAuthApiKeySnapsh
|
||||
snapshot.api_key_concurrent_limit = None;
|
||||
}
|
||||
|
||||
// Per-user list policy columns are retained only for legacy import/export compatibility.
|
||||
// Runtime authorization and user-facing catalogs must both treat group policies as authoritative.
|
||||
fn resolve_group_effective_list_policies(
|
||||
groups: &[aether_data::repository::users::StoredUserGroup],
|
||||
) -> GatewayUserEffectiveListPolicies {
|
||||
GatewayUserEffectiveListPolicies {
|
||||
allowed_providers: resolve_effective_list_policy(None, "unrestricted", groups, |group| {
|
||||
(
|
||||
&group.allowed_providers_mode,
|
||||
group.allowed_providers.clone(),
|
||||
)
|
||||
}),
|
||||
allowed_api_formats: resolve_effective_api_format_policy(
|
||||
None,
|
||||
"unrestricted",
|
||||
groups,
|
||||
|group| {
|
||||
(
|
||||
&group.allowed_api_formats_mode,
|
||||
group.allowed_api_formats.clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
allowed_models: resolve_effective_list_policy(None, "unrestricted", groups, |group| {
|
||||
(&group.allowed_models_mode, group.allowed_models.clone())
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_effective_list_policy(
|
||||
user_values: Option<Vec<String>>,
|
||||
user_mode: &str,
|
||||
@@ -1995,6 +1979,19 @@ fn resolve_effective_list_policy(
|
||||
intersect_list_policies(group_policy, user_policy)
|
||||
}
|
||||
|
||||
fn resolve_effective_api_format_policy(
|
||||
user_values: Option<Vec<String>>,
|
||||
user_mode: &str,
|
||||
groups: &[aether_data::repository::users::StoredUserGroup],
|
||||
group_field: impl Fn(
|
||||
&aether_data::repository::users::StoredUserGroup,
|
||||
) -> (&str, Option<Vec<String>>),
|
||||
) -> Option<Vec<String>> {
|
||||
let group_policy = union_group_list_policies(groups, group_field);
|
||||
let user_policy = list_restriction_from_mode(user_mode, user_values);
|
||||
intersect_api_format_list_policies(group_policy, user_policy)
|
||||
}
|
||||
|
||||
fn union_group_list_policies(
|
||||
groups: &[aether_data::repository::users::StoredUserGroup],
|
||||
group_field: impl Fn(
|
||||
@@ -2089,6 +2086,19 @@ fn intersect_list_policies(
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_api_format_list_policies(
|
||||
left: Option<Vec<String>>,
|
||||
right: Option<Vec<String>>,
|
||||
) -> Option<Vec<String>> {
|
||||
match (left, right) {
|
||||
(None, None) => None,
|
||||
(Some(values), None) | (None, Some(values)) => Some(values),
|
||||
(Some(left_values), Some(right_values)) => Some(
|
||||
crate::ai_serving::intersect_api_format_allowed_lists(&left_values, &right_values),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_rate_limit_policies(
|
||||
left: Option<RateLimitRestriction>,
|
||||
right: Option<RateLimitRestriction>,
|
||||
@@ -2133,22 +2143,6 @@ fn rate_limit_policy_value(policy: Option<RateLimitRestriction>) -> Option<i32>
|
||||
}
|
||||
}
|
||||
|
||||
fn constrain_api_key_list_policy_to_user_policy(
|
||||
user_policy: &mut Option<Vec<String>>,
|
||||
api_key_policy: &mut Option<Vec<String>>,
|
||||
) {
|
||||
let Some(api_key_values) = api_key_policy.as_ref().filter(|values| !values.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
let Some(user_values) = user_policy.clone() else {
|
||||
return;
|
||||
};
|
||||
let effective = intersect_list_policies(Some(api_key_values.to_vec()), Some(user_values))
|
||||
.unwrap_or_default();
|
||||
*user_policy = Some(effective.clone());
|
||||
*api_key_policy = Some(effective);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
@@ -2279,6 +2273,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_format_policy_intersection_preserves_search_companion_scope() {
|
||||
let mut responses_group =
|
||||
sample_group("responses", 10, None, "unrestricted", None, "system");
|
||||
responses_group.allowed_api_formats = Some(vec!["openai:responses".to_string()]);
|
||||
responses_group.allowed_api_formats_mode = "specific".to_string();
|
||||
|
||||
let search_policy = resolve_effective_api_format_policy(
|
||||
Some(vec!["openai:search".to_string()]),
|
||||
"specific",
|
||||
std::slice::from_ref(&responses_group),
|
||||
|group| {
|
||||
(
|
||||
&group.allowed_api_formats_mode,
|
||||
group.allowed_api_formats.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
assert_eq!(search_policy, Some(vec!["openai:search".to_string()]));
|
||||
|
||||
responses_group.allowed_api_formats = Some(vec!["openai:search".to_string()]);
|
||||
let responses_policy = resolve_effective_api_format_policy(
|
||||
Some(vec!["openai:responses".to_string()]),
|
||||
"specific",
|
||||
&[responses_group],
|
||||
|group| {
|
||||
(
|
||||
&group.allowed_api_formats_mode,
|
||||
group.allowed_api_formats.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
assert_eq!(responses_policy, Some(vec!["openai:search".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_policy_unions_multiple_group_restrictions_legacy_case() {
|
||||
let groups = vec![
|
||||
@@ -2452,17 +2481,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn api_key_specific_policy_cannot_expand_user_policy() {
|
||||
let mut user_policy = Some(vec!["gpt-5".to_string()]);
|
||||
let mut api_key_policy = Some(vec!["gpt-4.1".to_string()]);
|
||||
|
||||
constrain_api_key_list_policy_to_user_policy(&mut user_policy, &mut api_key_policy);
|
||||
|
||||
assert_eq!(user_policy, Some(Vec::<String>::new()));
|
||||
assert_eq!(api_key_policy, Some(Vec::<String>::new()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_non_standalone_snapshot_bypasses_group_and_key_policies() {
|
||||
let mut snapshot = sample_snapshot_with_role("key-admin", "admin-1", "admin")
|
||||
@@ -2518,6 +2536,38 @@ mod tests {
|
||||
assert_eq!(resolved.api_key_concurrent_limit, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_admin_role_bypasses_stored_user_and_key_policies() {
|
||||
let mut snapshot = sample_snapshot("key-admin", "admin-1");
|
||||
snapshot.api_key_allowed_providers = Some(vec!["anthropic".to_string()]);
|
||||
snapshot.api_key_allowed_api_formats = Some(vec!["anthropic:messages".to_string()]);
|
||||
snapshot.api_key_allowed_models = Some(vec!["claude-sonnet-4-5".to_string()]);
|
||||
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some("hash-admin".to_string()),
|
||||
snapshot,
|
||||
)]));
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
sample_auth_user("admin-1", "admin"),
|
||||
]));
|
||||
let state = GatewayDataState::with_auth_api_key_reader_for_tests(auth_repository)
|
||||
.with_user_reader(user_repository);
|
||||
|
||||
let resolved = state
|
||||
.read_auth_api_key_snapshot_by_key_hash("hash-admin", 100)
|
||||
.await
|
||||
.expect("snapshot should resolve")
|
||||
.expect("snapshot should exist");
|
||||
|
||||
assert_eq!(resolved.user_role, "admin");
|
||||
assert_eq!(resolved.effective_allowed_providers(), None);
|
||||
assert_eq!(resolved.effective_allowed_api_formats(), None);
|
||||
assert_eq!(resolved.effective_allowed_models(), None);
|
||||
assert_eq!(resolved.user_rate_limit, None);
|
||||
assert_eq!(resolved.api_key_rate_limit, None);
|
||||
assert_eq!(resolved.api_key_concurrent_limit, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_personal_policy_fields_are_ignored_when_groups_are_applied() {
|
||||
let mut snapshot = sample_snapshot("key-user", "user-1").with_user_rate_limit(Some(200));
|
||||
@@ -2529,8 +2579,9 @@ mod tests {
|
||||
Some("hash-user".to_string()),
|
||||
snapshot,
|
||||
)]));
|
||||
let user = sample_auth_user("user-1", "user");
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
sample_auth_user("user-1", "user"),
|
||||
user.clone()
|
||||
]));
|
||||
let group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
@@ -2575,6 +2626,110 @@ mod tests {
|
||||
Some(&["claude-sonnet-4-5".to_string()][..])
|
||||
);
|
||||
assert_eq!(resolved.user_rate_limit, Some(30));
|
||||
|
||||
let catalog_policies = state
|
||||
.resolve_user_effective_list_policies(&user)
|
||||
.await
|
||||
.expect("catalog policies should resolve");
|
||||
assert_eq!(
|
||||
catalog_policies.allowed_providers.as_deref(),
|
||||
resolved.effective_allowed_providers()
|
||||
);
|
||||
assert_eq!(
|
||||
catalog_policies.allowed_api_formats.as_deref(),
|
||||
resolved.effective_allowed_api_formats()
|
||||
);
|
||||
assert_eq!(
|
||||
catalog_policies.allowed_models.as_deref(),
|
||||
resolved.effective_allowed_models()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn group_responses_permission_and_key_search_scope_resolve_to_search() {
|
||||
let mut snapshot = sample_snapshot("key-search", "user-search");
|
||||
snapshot.api_key_allowed_api_formats = Some(vec!["openai:search".to_string()]);
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some("hash-search".to_string()),
|
||||
snapshot,
|
||||
)]));
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
sample_auth_user("user-search", "user"),
|
||||
]));
|
||||
let group = user_repository
|
||||
.create_user_group(UpsertUserGroupRecord {
|
||||
name: "Responses".to_string(),
|
||||
description: None,
|
||||
priority: 10,
|
||||
allowed_providers: None,
|
||||
allowed_providers_mode: "unrestricted".to_string(),
|
||||
allowed_api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
allowed_api_formats_mode: "specific".to_string(),
|
||||
allowed_models: None,
|
||||
allowed_models_mode: "unrestricted".to_string(),
|
||||
rate_limit: None,
|
||||
rate_limit_mode: "system".to_string(),
|
||||
})
|
||||
.await
|
||||
.expect("group should create")
|
||||
.expect("group should exist");
|
||||
user_repository
|
||||
.add_user_to_group(&group.id, "user-search")
|
||||
.await
|
||||
.expect("group membership should create");
|
||||
|
||||
let state = GatewayDataState::with_auth_api_key_reader_for_tests(auth_repository)
|
||||
.with_user_reader(user_repository);
|
||||
let resolved = state
|
||||
.read_auth_api_key_snapshot_by_key_hash("hash-search", 100)
|
||||
.await
|
||||
.expect("snapshot should resolve")
|
||||
.expect("snapshot should exist");
|
||||
|
||||
assert_eq!(
|
||||
resolved.effective_allowed_api_formats(),
|
||||
Some(&["openai:search".to_string()][..])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn snapshot_without_user_reader_uses_stored_policy_intersection() {
|
||||
let mut snapshot = sample_snapshot("key-search", "user-search");
|
||||
snapshot.api_key_allowed_api_formats = Some(vec!["openai:search".to_string()]);
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some("hash-search".to_string()),
|
||||
snapshot,
|
||||
)]));
|
||||
let state = GatewayDataState::with_auth_api_key_reader_for_tests(auth_repository);
|
||||
|
||||
let resolved = state
|
||||
.read_auth_api_key_snapshot_by_key_hash("hash-search", 100)
|
||||
.await
|
||||
.expect("snapshot should resolve")
|
||||
.expect("snapshot should exist");
|
||||
|
||||
assert_eq!(resolved.effective_allowed_api_formats(), Some(&[][..]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn missing_current_user_uses_stored_policy_intersection() {
|
||||
let mut snapshot = sample_snapshot("key-search", "missing-user");
|
||||
snapshot.api_key_allowed_api_formats = Some(vec!["openai:search".to_string()]);
|
||||
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||||
Some("hash-search".to_string()),
|
||||
snapshot,
|
||||
)]));
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::default());
|
||||
let state = GatewayDataState::with_auth_api_key_reader_for_tests(auth_repository)
|
||||
.with_user_reader(user_repository);
|
||||
|
||||
let resolved = state
|
||||
.read_auth_api_key_snapshot_by_key_hash("hash-search", 100)
|
||||
.await
|
||||
.expect("snapshot should resolve")
|
||||
.expect("snapshot should exist");
|
||||
|
||||
assert_eq!(resolved.effective_allowed_api_formats(), Some(&[][..]));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -546,6 +546,88 @@ impl GatewayDataState {
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn upsert_provider_catalog_key_upstream_metadata_namespace(
|
||||
&self,
|
||||
key_id: &str,
|
||||
namespace: &str,
|
||||
value: &serde_json::Value,
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> Result<bool, DataLayerError> {
|
||||
let updated = match &self.provider_catalog_writer {
|
||||
Some(repository) => {
|
||||
repository
|
||||
.upsert_key_upstream_metadata_namespace(
|
||||
key_id,
|
||||
namespace,
|
||||
value,
|
||||
updated_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Ok(false),
|
||||
}?;
|
||||
if updated {
|
||||
self.clear_provider_catalog_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_key_model_fetch_state(
|
||||
&self,
|
||||
key_id: &str,
|
||||
allowed_models: Option<&serde_json::Value>,
|
||||
last_models_fetch_at_unix_secs: Option<u64>,
|
||||
last_models_fetch_error: Option<&str>,
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> Result<bool, DataLayerError> {
|
||||
let updated = match &self.provider_catalog_writer {
|
||||
Some(repository) => {
|
||||
repository
|
||||
.update_key_model_fetch_state(
|
||||
key_id,
|
||||
allowed_models,
|
||||
last_models_fetch_at_unix_secs,
|
||||
last_models_fetch_error,
|
||||
updated_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Ok(false),
|
||||
}?;
|
||||
if updated {
|
||||
self.clear_provider_catalog_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_provider_catalog_key_model_fetch_success(
|
||||
&self,
|
||||
key_id: &str,
|
||||
allowed_models: Option<&serde_json::Value>,
|
||||
last_models_fetch_at_unix_secs: u64,
|
||||
upstream_metadata_updates: &[aether_data_contracts::repository::provider_catalog::ProviderCatalogUpstreamMetadataNamespaceUpdate],
|
||||
updated_at_unix_secs: Option<u64>,
|
||||
) -> Result<bool, DataLayerError> {
|
||||
let updated = match &self.provider_catalog_writer {
|
||||
Some(repository) => {
|
||||
repository
|
||||
.update_key_model_fetch_success(
|
||||
key_id,
|
||||
allowed_models,
|
||||
last_models_fetch_at_unix_secs,
|
||||
upstream_metadata_updates,
|
||||
updated_at_unix_secs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Ok(false),
|
||||
}?;
|
||||
if updated {
|
||||
self.clear_provider_catalog_cache();
|
||||
}
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_provider_catalog_key(
|
||||
&self,
|
||||
key_id: &str,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,8 @@ use aether_data_contracts::repository::usage::{
|
||||
PendingUsageCleanupSummary, ProviderApiKeyWindowUsageRequest,
|
||||
StoredProviderApiKeyWindowUsageSummary, StoredUsageDailySummary, UsageAuditListQuery,
|
||||
UsageCleanupExecutionMode, UsageCleanupSummary, UsageCleanupTargets, UsageCleanupWindow,
|
||||
UsageCounterFlushSummary, UsageCounterHealthSnapshot, UsageDailyHeatmapQuery,
|
||||
UsageCounterFlushSummary, UsageCounterHealthSnapshot, UsageCounterPendingHealthSnapshot,
|
||||
UsageDailyHeatmapQuery,
|
||||
};
|
||||
use aether_runtime_state::RuntimeQueueStore;
|
||||
use aether_video_tasks_core::read_data_backed_video_task_response;
|
||||
@@ -152,6 +153,13 @@ impl GatewayDataState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn warm_database_pool(&self) -> Result<(), DataLayerError> {
|
||||
match &self.backends {
|
||||
Some(backends) => backends.warm_database_pool().await,
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn pending_database_backfills(
|
||||
&self,
|
||||
) -> Result<
|
||||
@@ -198,15 +206,22 @@ impl GatewayDataState {
|
||||
pub(crate) fn database_pool_summary_under_maintenance_pressure(
|
||||
summary: &aether_data::DatabasePoolSummary,
|
||||
) -> bool {
|
||||
summary.checked_out > 0 && summary.idle <= Self::maintenance_pool_idle_reserve(summary)
|
||||
summary.checked_out > 0
|
||||
&& Self::database_pool_available_capacity(summary)
|
||||
<= Self::maintenance_pool_idle_reserve(summary)
|
||||
}
|
||||
|
||||
pub(crate) fn database_pool_summary_under_usage_worker_pressure(
|
||||
summary: &aether_data::DatabasePoolSummary,
|
||||
) -> bool {
|
||||
summary.checked_out > 0
|
||||
&& (summary.checked_out >= summary.max_connections as usize
|
||||
|| summary.idle <= Self::usage_worker_pool_idle_reserve(summary))
|
||||
&& Self::database_pool_available_capacity(summary)
|
||||
<= Self::usage_worker_pool_idle_reserve(summary)
|
||||
}
|
||||
|
||||
fn database_pool_available_capacity(summary: &aether_data::DatabasePoolSummary) -> usize {
|
||||
let unopened = (summary.max_connections as usize).saturating_sub(summary.pool_size);
|
||||
summary.idle.saturating_add(unopened)
|
||||
}
|
||||
|
||||
pub(crate) fn maintenance_pool_idle_reserve(
|
||||
@@ -1353,6 +1368,15 @@ impl GatewayDataState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_usage_counter_pending_health(
|
||||
&self,
|
||||
) -> Result<UsageCounterPendingHealthSnapshot, DataLayerError> {
|
||||
match &self.usage_reader {
|
||||
Some(repository) => repository.read_usage_counter_pending_health().await,
|
||||
None => Ok(UsageCounterPendingHealthSnapshot::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_usage_totals_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
@@ -1439,6 +1463,22 @@ impl GatewayDataState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn summarize_dashboard_stats(
|
||||
&self,
|
||||
query: &aether_data_contracts::repository::usage::UsageDashboardSummaryQuery,
|
||||
) -> Result<
|
||||
aether_data_contracts::repository::usage::StoredUsageDashboardStatsSummary,
|
||||
DataLayerError,
|
||||
> {
|
||||
match &self.usage_reader {
|
||||
Some(repository) => repository.summarize_dashboard_stats(query).await,
|
||||
None => Ok(
|
||||
aether_data_contracts::repository::usage::StoredUsageDashboardStatsSummary::default(
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn list_dashboard_daily_breakdown(
|
||||
&self,
|
||||
query: &aether_data_contracts::repository::usage::UsageDashboardDailyBreakdownQuery,
|
||||
|
||||
Reference in New Issue
Block a user