mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: scope planner affinity cache by session
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use aether_scheduler_core::{
|
use aether_scheduler_core::{
|
||||||
build_scheduler_affinity_cache_key_for_api_key_id, SchedulerAffinityTarget,
|
build_scheduler_affinity_cache_key_for_api_key_id_with_client_session, ClientSessionAffinity,
|
||||||
SchedulerMinimalCandidateSelectionCandidate,
|
SchedulerAffinityTarget, SchedulerMinimalCandidateSelectionCandidate,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
|
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
|
||||||
@@ -11,6 +11,7 @@ const PLANNER_SCHEDULER_AFFINITY_MAX_ENTRIES: usize = 10_000;
|
|||||||
pub(crate) fn read_cached_scheduler_affinity_target(
|
pub(crate) fn read_cached_scheduler_affinity_target(
|
||||||
state: PlannerAppState<'_>,
|
state: PlannerAppState<'_>,
|
||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
||||||
client_api_format: &str,
|
client_api_format: &str,
|
||||||
requested_model: Option<&str>,
|
requested_model: Option<&str>,
|
||||||
) -> Option<SchedulerAffinityTarget> {
|
) -> Option<SchedulerAffinityTarget> {
|
||||||
@@ -20,10 +21,11 @@ pub(crate) fn read_cached_scheduler_affinity_target(
|
|||||||
let api_key_id = auth_snapshot
|
let api_key_id = auth_snapshot
|
||||||
.map(|snapshot| snapshot.api_key_id.trim())
|
.map(|snapshot| snapshot.api_key_id.trim())
|
||||||
.filter(|value| !value.is_empty())?;
|
.filter(|value| !value.is_empty())?;
|
||||||
let cache_key = build_scheduler_affinity_cache_key_for_api_key_id(
|
let cache_key = build_scheduler_affinity_cache_key_for_api_key_id_with_client_session(
|
||||||
api_key_id,
|
api_key_id,
|
||||||
client_api_format,
|
client_api_format,
|
||||||
requested_model,
|
requested_model,
|
||||||
|
client_session_affinity,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
state
|
state
|
||||||
@@ -34,6 +36,7 @@ pub(crate) fn read_cached_scheduler_affinity_target(
|
|||||||
pub(crate) fn remember_scheduler_affinity_for_candidate(
|
pub(crate) fn remember_scheduler_affinity_for_candidate(
|
||||||
state: PlannerAppState<'_>,
|
state: PlannerAppState<'_>,
|
||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
||||||
client_api_format: &str,
|
client_api_format: &str,
|
||||||
requested_model: &str,
|
requested_model: &str,
|
||||||
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
candidate: &SchedulerMinimalCandidateSelectionCandidate,
|
||||||
@@ -44,10 +47,11 @@ pub(crate) fn remember_scheduler_affinity_for_candidate(
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(cache_key) = build_scheduler_affinity_cache_key_for_api_key_id(
|
let Some(cache_key) = build_scheduler_affinity_cache_key_for_api_key_id_with_client_session(
|
||||||
api_key_id,
|
api_key_id,
|
||||||
client_api_format,
|
client_api_format,
|
||||||
requested_model,
|
requested_model,
|
||||||
|
client_session_affinity,
|
||||||
) else {
|
) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ use crate::scheduler::config::{
|
|||||||
read_scheduler_ordering_config, SchedulerOrderingConfig, SchedulerSchedulingMode,
|
read_scheduler_ordering_config, SchedulerOrderingConfig, SchedulerSchedulingMode,
|
||||||
};
|
};
|
||||||
use aether_scheduler_core::{
|
use aether_scheduler_core::{
|
||||||
matches_affinity_target, SchedulerAffinityTarget, SchedulerMinimalCandidateSelectionCandidate,
|
matches_affinity_target, ClientSessionAffinity, SchedulerAffinityTarget,
|
||||||
SchedulerRankableCandidate, SchedulerRankingContext, SchedulerRankingOutcome,
|
SchedulerMinimalCandidateSelectionCandidate, SchedulerRankableCandidate,
|
||||||
|
SchedulerRankingContext, SchedulerRankingOutcome,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::candidate_affinity_cache::read_cached_scheduler_affinity_target;
|
use super::candidate_affinity_cache::read_cached_scheduler_affinity_target;
|
||||||
@@ -28,6 +29,7 @@ struct GatewayLocalCandidateRankingPort<'a> {
|
|||||||
state: PlannerAppState<'a>,
|
state: PlannerAppState<'a>,
|
||||||
requested_model: Option<&'a str>,
|
requested_model: Option<&'a str>,
|
||||||
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&'a GatewayAuthApiKeySnapshot>,
|
||||||
|
client_session_affinity: Option<&'a ClientSessionAffinity>,
|
||||||
required_capabilities: Option<&'a serde_json::Value>,
|
required_capabilities: Option<&'a serde_json::Value>,
|
||||||
ordering_config: SchedulerOrderingConfig,
|
ordering_config: SchedulerOrderingConfig,
|
||||||
}
|
}
|
||||||
@@ -58,6 +60,7 @@ impl AiCandidateRankingPort for GatewayLocalCandidateRankingPort<'_> {
|
|||||||
Ok(read_cached_scheduler_affinity_target(
|
Ok(read_cached_scheduler_affinity_target(
|
||||||
self.state,
|
self.state,
|
||||||
self.auth_snapshot,
|
self.auth_snapshot,
|
||||||
|
self.client_session_affinity,
|
||||||
normalized_client_api_format,
|
normalized_client_api_format,
|
||||||
affinity_requested_model,
|
affinity_requested_model,
|
||||||
))
|
))
|
||||||
@@ -116,6 +119,7 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
|
|||||||
normalized_client_api_format: &str,
|
normalized_client_api_format: &str,
|
||||||
requested_model: Option<&str>,
|
requested_model: Option<&str>,
|
||||||
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
|
||||||
|
client_session_affinity: Option<&ClientSessionAffinity>,
|
||||||
required_capabilities: Option<&serde_json::Value>,
|
required_capabilities: Option<&serde_json::Value>,
|
||||||
) -> Vec<EligibleLocalExecutionCandidate> {
|
) -> Vec<EligibleLocalExecutionCandidate> {
|
||||||
let ordering_config = read_scheduler_ordering_config_or_default(state).await;
|
let ordering_config = read_scheduler_ordering_config_or_default(state).await;
|
||||||
@@ -123,6 +127,7 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
|
|||||||
state,
|
state,
|
||||||
requested_model,
|
requested_model,
|
||||||
auth_snapshot,
|
auth_snapshot,
|
||||||
|
client_session_affinity,
|
||||||
required_capabilities,
|
required_capabilities,
|
||||||
ordering_config,
|
ordering_config,
|
||||||
};
|
};
|
||||||
@@ -212,7 +217,9 @@ mod tests {
|
|||||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||||
};
|
};
|
||||||
use aether_scheduler_core::{
|
use aether_scheduler_core::{
|
||||||
apply_scheduler_candidate_ranking, RANKING_REASON_CACHED_AFFINITY,
|
apply_scheduler_candidate_ranking,
|
||||||
|
build_scheduler_affinity_cache_key_for_api_key_id_with_client_session,
|
||||||
|
ClientSessionAffinity, RANKING_REASON_CACHED_AFFINITY,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -1052,6 +1059,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1130,6 +1138,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1204,6 +1213,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1269,6 +1279,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1350,6 +1361,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1398,6 +1410,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
&cached_candidate,
|
&cached_candidate,
|
||||||
@@ -1422,6 +1435,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1498,6 +1512,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1567,6 +1582,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"claude:messages",
|
"claude:messages",
|
||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
&cached_cross_format,
|
&cached_cross_format,
|
||||||
@@ -1591,6 +1607,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1660,6 +1677,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
&cached_candidate,
|
&cached_candidate,
|
||||||
@@ -1692,6 +1710,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1762,6 +1781,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
&cached_candidate,
|
&cached_candidate,
|
||||||
@@ -1786,6 +1806,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
aether_ai_serving::AiCandidateResolutionMode::Standard,
|
aether_ai_serving::AiCandidateResolutionMode::Standard,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -1852,6 +1873,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
"gpt-4.1",
|
"gpt-4.1",
|
||||||
&cached_candidate,
|
&cached_candidate,
|
||||||
@@ -1876,6 +1898,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
aether_ai_serving::AiCandidateResolutionMode::Standard,
|
aether_ai_serving::AiCandidateResolutionMode::Standard,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -1902,6 +1925,7 @@ mod tests {
|
|||||||
remember_scheduler_affinity_for_candidate(
|
remember_scheduler_affinity_for_candidate(
|
||||||
PlannerAppState::new(&state),
|
PlannerAppState::new(&state),
|
||||||
Some(&auth_snapshot),
|
Some(&auth_snapshot),
|
||||||
|
None,
|
||||||
"openai:chat",
|
"openai:chat",
|
||||||
"gpt-5",
|
"gpt-5",
|
||||||
&candidate,
|
&candidate,
|
||||||
@@ -1917,4 +1941,44 @@ mod tests {
|
|||||||
assert_eq!(remembered.endpoint_id, "endpoint-1");
|
assert_eq!(remembered.endpoint_id, "endpoint-1");
|
||||||
assert_eq!(remembered.key_id, "key-1");
|
assert_eq!(remembered.key_id, "key-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remembers_scheduler_affinity_for_client_session_scope() {
|
||||||
|
let state = AppState::new().expect("state should build");
|
||||||
|
let auth_snapshot = sample_auth_snapshot();
|
||||||
|
let client_session_affinity = ClientSessionAffinity::new(
|
||||||
|
Some("generic".to_string()),
|
||||||
|
Some("session=conversation-1;agent=coder".to_string()),
|
||||||
|
);
|
||||||
|
let candidate = sample_candidate("endpoint-session", "key-session");
|
||||||
|
|
||||||
|
remember_scheduler_affinity_for_candidate(
|
||||||
|
PlannerAppState::new(&state),
|
||||||
|
Some(&auth_snapshot),
|
||||||
|
Some(&client_session_affinity),
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-5",
|
||||||
|
&candidate,
|
||||||
|
);
|
||||||
|
|
||||||
|
let session_key = build_scheduler_affinity_cache_key_for_api_key_id_with_client_session(
|
||||||
|
"api-key-1",
|
||||||
|
"openai:chat",
|
||||||
|
"gpt-5",
|
||||||
|
Some(&client_session_affinity),
|
||||||
|
)
|
||||||
|
.expect("session key should build");
|
||||||
|
let remembered = state
|
||||||
|
.read_scheduler_affinity_target(&session_key, SCHEDULER_AFFINITY_TTL)
|
||||||
|
.expect("session affinity target should be cached");
|
||||||
|
assert_eq!(remembered.provider_id, "provider-1");
|
||||||
|
assert_eq!(remembered.endpoint_id, "endpoint-session");
|
||||||
|
assert_eq!(remembered.key_id, "key-session");
|
||||||
|
assert!(state
|
||||||
|
.read_scheduler_affinity_target(
|
||||||
|
"scheduler_affinity:api-key-1:openai:chat:gpt-5",
|
||||||
|
SCHEDULER_AFFINITY_TTL,
|
||||||
|
)
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user