Improve gateway transport and usage runtime

This commit is contained in:
elky
2026-06-25 22:36:27 +08:00
parent d336d1a7fa
commit 6f00e9fc67
112 changed files with 12456 additions and 1387 deletions
Generated
+5
View File
@@ -258,7 +258,11 @@ dependencies = [
"futures-util",
"hmac",
"http",
"http-body-util",
"hyper",
"hyper-util",
"ldap3",
"libc",
"md-5",
"object_store",
"parking_lot",
@@ -2205,6 +2209,7 @@ dependencies = [
"pin-project-lite",
"socket2 0.6.3",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
+3
View File
@@ -30,6 +30,9 @@ members = [
"crates/aether-runtime",
"crates/aether-testkit",
]
default-members = [
"apps/aether-gateway",
]
resolver = "2"
[workspace.package]
+2 -2
View File
@@ -62,7 +62,7 @@ COPY --from=gateway-planner /build/recipe.json ./recipe.json
RUN --mount=type=cache,id=aether-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=aether-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=aether-cargo-target-local,target=/build/target,sharing=locked \
cargo chef cook --release --locked --package aether-gateway --bin aether-gateway --recipe-path recipe.json
cargo chef cook --release --locked --package aether-gateway --bin aether-gateway --features jemalloc --recipe-path recipe.json
COPY Cargo.toml Cargo.lock ./
COPY apps/ ./apps/
@@ -71,7 +71,7 @@ RUN --mount=type=cache,id=aether-cargo-registry,target=/usr/local/cargo/registry
--mount=type=cache,id=aether-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=aether-cargo-target-local,target=/build/target,sharing=locked \
set -eux; \
cargo build --release --locked -p aether-gateway --bin aether-gateway; \
cargo build --release --locked -p aether-gateway --bin aether-gateway --features jemalloc; \
cp target/release/aether-gateway /tmp/aether-gateway
# ==================== 最小运行时打包 ====================
+2 -2
View File
@@ -60,7 +60,7 @@ COPY --from=gateway-planner /build/recipe.json ./recipe.json
RUN --mount=type=cache,id=aether-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=aether-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=aether-cargo-target-release-local,target=/build/target,sharing=locked \
cargo chef cook --release --locked --package aether-gateway --bin aether-gateway --recipe-path recipe.json
cargo chef cook --release --locked --package aether-gateway --bin aether-gateway --features jemalloc --recipe-path recipe.json
COPY Cargo.toml Cargo.lock ./
COPY apps/ ./apps/
@@ -68,7 +68,7 @@ COPY crates/ ./crates/
RUN --mount=type=cache,id=aether-cargo-registry,target=/usr/local/cargo/registry,sharing=locked \
--mount=type=cache,id=aether-cargo-git,target=/usr/local/cargo/git,sharing=locked \
--mount=type=cache,id=aether-cargo-target-release-local,target=/build/target,sharing=locked \
cargo build --release --locked -p aether-gateway && \
cargo build --release --locked -p aether-gateway --features jemalloc && \
cp target/release/aether-gateway /tmp/aether-gateway
# ==================== 最小运行时打包 ====================
+10 -1
View File
@@ -6,6 +6,11 @@ license.workspace = true
repository.workspace = true
description = "Rust ingress gateway for Aether phase 3a transparent proxy"
[features]
default = []
jemalloc = ["dep:tikv-jemallocator"]
testkit = []
[dependencies]
aether-admin.workspace = true
aether-ai-formats.workspace = true
@@ -46,7 +51,11 @@ flate2.workspace = true
futures-util.workspace = true
hmac.workspace = true
http.workspace = true
http-body-util = "0.1"
hyper = { version = "1", features = ["client", "server", "http1", "http2"] }
hyper-util = { version = "0.1", features = ["client-legacy", "client-pool", "server-auto", "service", "tokio"] }
ldap3 = { version = "0.11", default-features = false, features = ["sync", "tls-rustls"] }
libc = "0.2"
md-5 = "0.10"
object_store.workspace = true
parking_lot = "0.12"
@@ -75,7 +84,7 @@ wreq-util.workspace = true
zstd.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator = "0.6"
tikv-jemallocator = { version = "0.6", optional = true }
[dev-dependencies]
aether-testkit.workspace = true
+9 -6
View File
@@ -55,12 +55,15 @@ pub(crate) use aether_ai_formats::api::{
ExecutionRuntimeAuthContext, LocalCoreSyncErrorKind, LocalOpenAiImageSpec,
LocalSameFormatProviderFamily, LocalSameFormatProviderSpec, LocalStandardSourceFamily,
LocalStandardSourceMode, LocalStandardSpec, OpenAIChatClientEmitter,
OpenAIResponsesClientEmitter, StreamingStandardTerminalObserver,
EXECUTION_RUNTIME_STREAM_DECISION_ACTION, EXECUTION_RUNTIME_SYNC_DECISION_ACTION,
GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_EMBEDDING_SYNC_PLAN_KIND,
OPENAI_IMAGE_STREAM_PLAN_KIND, OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_PLAN_KIND, OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OpenAIResponsesClientEmitter, StreamingStandardTerminalObserver, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CLI_STREAM_PLAN_KIND, EXECUTION_RUNTIME_STREAM_DECISION_ACTION,
EXECUTION_RUNTIME_SYNC_DECISION_ACTION, GEMINI_CHAT_STREAM_PLAN_KIND,
GEMINI_CLI_STREAM_PLAN_KIND, GEMINI_EMBEDDING_SYNC_PLAN_KIND, GEMINI_FILES_DOWNLOAD_PLAN_KIND,
GEMINI_VIDEO_CANCEL_SYNC_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND,
OPENAI_EMBEDDING_SYNC_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND, OPENAI_IMAGE_SYNC_PLAN_KIND,
OPENAI_RERANK_SYNC_PLAN_KIND, OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND,
OPENAI_RESPONSES_STREAM_PLAN_KIND, OPENAI_VIDEO_CANCEL_SYNC_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND, OPENAI_VIDEO_DELETE_SYNC_PLAN_KIND,
OPENAI_VIDEO_REMIX_SYNC_PLAN_KIND,
};
@@ -740,6 +740,7 @@ where
request_auth_channel,
use_api_format_alias_match,
key_mode,
sticky_session_token.is_none(),
Some(trace_id),
)
.await;
@@ -2188,6 +2189,7 @@ mod tests {
None,
false,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModel,
true,
Some("trace-no-session-affinity"),
)
.await;
@@ -2241,6 +2243,7 @@ mod tests {
None,
false,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModel,
true,
Some("trace-session-affinity"),
)
.await;
@@ -2276,6 +2279,7 @@ mod tests {
None,
false,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModel,
true,
Some("trace-fixed-order"),
)
.await;
@@ -12,18 +12,29 @@ use aether_scheduler_core::{
};
use async_trait::async_trait;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::sync::Arc;
use crate::ai_serving::planner::candidate_affinity_cache::has_explicit_session_affinity;
use crate::ai_serving::planner::candidate_resolution::SkippedLocalExecutionCandidate;
use crate::ai_serving::{GatewayAuthApiKeySnapshot, PlannerAppState};
use crate::cache::{
candidate_page_cache_stale_ttl, candidate_page_cache_ttl_from_env,
record_candidate_page_cache_follower_wait, record_candidate_page_cache_hit,
record_candidate_page_cache_load, record_candidate_page_cache_miss,
record_candidate_page_cache_none, record_candidate_row_page_cache_follower_wait,
record_candidate_row_page_cache_hit, record_candidate_row_page_cache_load,
record_candidate_row_page_cache_miss, record_candidate_row_page_cache_none, CacheLoadObserver,
CandidatePageCacheKey, CandidatePageSnapshot, CandidateRowPageCacheKey,
};
use crate::clock::request_distribution_seed;
use crate::data::candidate_selection::{
read_requested_model_rows_fast_path_page, requested_model_candidate_names,
MinimalCandidateSelectionRowSource, REQUESTED_MODEL_CANDIDATE_PAGE_SIZE,
REQUESTED_MODEL_MAX_SCANNED_ROWS,
MinimalCandidateSelectionRowSource, RequestedModelCandidateRowsPage,
REQUESTED_MODEL_CANDIDATE_PAGE_SIZE, REQUESTED_MODEL_MAX_SCANNED_ROWS,
};
use crate::scheduler::candidate::SchedulerSkippedCandidate;
use crate::scheduler::config::{SchedulerOrderingConfig, SchedulerSchedulingMode};
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::GatewayError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -258,6 +269,7 @@ pub(crate) struct LocalCandidatePreselectionPageCursor<'a> {
request_auth_channel: Option<String>,
use_api_format_alias_match: bool,
key_mode: LocalCandidatePreselectionKeyMode,
allow_priority_page_cache: bool,
candidate_api_formats: Vec<String>,
model_directive_enabled_api_formats: BTreeSet<String>,
ordering_config: SchedulerOrderingConfig,
@@ -295,6 +307,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
request_auth_channel: Option<&str>,
use_api_format_alias_match: bool,
key_mode: LocalCandidatePreselectionKeyMode,
allow_priority_page_cache: bool,
trace_id: Option<&str>,
) -> Self {
let candidate_api_formats =
@@ -336,6 +349,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
request_auth_channel: request_auth_channel.map(str::to_string),
use_api_format_alias_match,
key_mode,
allow_priority_page_cache,
candidate_api_formats,
model_directive_enabled_api_formats,
ordering_config,
@@ -429,6 +443,10 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
}
}
fn should_cache_current_priority_page(&self) -> bool {
self.allow_priority_page_cache && self.should_cache_current_priority_resolved_page()
}
#[cfg(test)]
pub(crate) fn mark_priority_page_emitted_for_tests(&mut self) {
self.priority_page_emitted = true;
@@ -443,11 +461,75 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
>,
GatewayError,
> {
let page = self.next_priority_page_with_planning_gate().await?;
let page = if self.should_cache_current_priority_page() {
self.cached_next_priority_page_snapshot().await?
} else {
self.next_priority_page_with_planning_gate().await?
};
self.remember_seen_candidates_from_page(&page);
Ok(page)
}
async fn cached_next_priority_page_snapshot(
&mut self,
) -> Result<
AiCandidatePreselectionOutcome<
SchedulerMinimalCandidateSelectionCandidate,
SkippedLocalExecutionCandidate,
>,
GatewayError,
> {
let key = CandidatePageCacheKey::new(
&self.requested_model,
&self.client_api_format,
self.require_streaming,
&self.auth_snapshot,
self.required_capabilities.as_ref(),
self.routing_policy.as_ref(),
self.request_auth_channel.as_deref(),
self.state.app().scheduler_affinity_epoch(),
self.key_mode.cache_key_name(),
self.use_api_format_alias_match,
self.client_session_affinity.as_ref(),
);
let cache = self.state.app().candidate_page_cache.clone();
let ttl = candidate_page_cache_ttl_from_env();
let stale_ttl = candidate_page_cache_stale_ttl(ttl);
let cached = cache
.get_or_load_once_stale_while_refreshing(
key,
ttl,
stale_ttl,
|| async {
let page = self.next_priority_page_with_planning_gate().await?;
Ok::<_, GatewayError>(Some(Arc::new(page) as Arc<CandidatePageSnapshot>))
},
CacheLoadObserver::new()
.on_hit(record_candidate_page_cache_hit)
.on_miss(record_candidate_page_cache_miss)
.on_load(record_candidate_page_cache_load)
.on_follower_wait(record_candidate_page_cache_follower_wait),
)
.await?;
match cached {
Some(snapshot) => {
let page = snapshot.as_ref().clone();
if page.candidates.is_empty() && page.skipped_candidates.is_empty() {
record_candidate_page_cache_none();
}
Ok(page)
}
None => {
record_candidate_page_cache_none();
Ok(AiCandidatePreselectionOutcome {
candidates: Vec::new(),
skipped_candidates: Vec::new(),
})
}
}
}
fn remember_seen_candidates_from_page(
&mut self,
page: &AiCandidatePreselectionOutcome<
@@ -717,17 +799,15 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
return Ok(None);
}
let limit = REQUESTED_MODEL_CANDIDATE_PAGE_SIZE.min(remaining);
let page = read_requested_model_rows_fast_path_page(
self.state.app().data.as_ref(),
&normalized_api_format,
&self.requested_model,
requested_name,
offset,
limit,
enable_model_directives,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let page = self
.read_requested_model_rows_fast_path_page_cached(
&normalized_api_format,
requested_name,
offset,
limit,
enable_model_directives,
)
.await?;
self.scanned_rows_by_format.insert(
normalized_api_format.clone(),
scanned.saturating_add(page.scanned_rows),
@@ -765,6 +845,70 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
}
}
async fn read_requested_model_rows_fast_path_page_cached(
&self,
normalized_api_format: &str,
requested_name: &str,
offset: u32,
limit: u32,
enable_model_directives: bool,
) -> Result<RequestedModelCandidateRowsPage, GatewayError> {
let key = CandidateRowPageCacheKey::new(
normalized_api_format,
&self.requested_model,
requested_name,
offset,
limit,
enable_model_directives,
);
let cache = self.state.app().candidate_row_page_cache.clone();
let ttl = candidate_page_cache_ttl_from_env();
let stale_ttl = candidate_page_cache_stale_ttl(ttl);
let cached = cache
.get_or_load_once_stale_while_refreshing(
key,
ttl,
stale_ttl,
|| async {
let page = read_requested_model_rows_fast_path_page(
self.state.app().data.as_ref(),
normalized_api_format,
&self.requested_model,
requested_name,
offset,
limit,
enable_model_directives,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok::<_, GatewayError>(Some(Arc::new(page)))
},
CacheLoadObserver::new()
.on_hit(record_candidate_row_page_cache_hit)
.on_miss(record_candidate_row_page_cache_miss)
.on_load(record_candidate_row_page_cache_load)
.on_follower_wait(record_candidate_row_page_cache_follower_wait),
)
.await?;
match cached {
Some(page) => {
if page.rows.is_empty() {
record_candidate_row_page_cache_none();
}
Ok(page.as_ref().clone())
}
None => {
record_candidate_row_page_cache_none();
Ok(RequestedModelCandidateRowsPage {
rows: Vec::new(),
scanned_rows: 0,
end_of_requested_name: true,
})
}
}
}
async fn next_fallback_page_for_api_format(
&mut self,
candidate_api_format: &str,
@@ -1022,8 +1166,15 @@ async fn acquire_candidate_planning_gate(
.app()
.frontdoor_runtime_guards
.internal_gate_queue_budget;
let gate_wait_started_at = std::time::Instant::now();
match tokio::time::timeout(budget, gate.acquire()).await {
Ok(Ok(permit)) => Ok(Some(permit)),
Ok(Ok(permit)) => {
observe_gateway_stage_ms(
"candidate_planning_gate_wait",
gate_wait_started_at.elapsed().as_millis() as u64,
);
Ok(Some(permit))
}
Ok(Err(err)) => Err(GatewayError::Internal(err.to_string())),
Err(_) => Err(GatewayError::AdmissionTimeout {
trace_id: trace_id.to_string(),
@@ -1140,6 +1291,50 @@ mod tests {
}
}
#[tokio::test]
async fn priority_page_cache_requires_fixed_order_or_explicit_affinity() {
let repository: Arc<dyn MinimalCandidateSelectionReadRepository> =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(
Vec::<StoredMinimalCandidateSelectionRow>::new(),
));
let data_state =
GatewayDataState::with_minimal_candidate_selection_reader_for_tests(repository);
let app = AppState::new()
.expect("gateway state should build")
.with_data_state_for_tests(data_state);
let auth_snapshot = unrestricted_auth_snapshot();
let mut cursor = LocalCandidatePreselectionPageCursor::new(
PlannerAppState::new(&app),
"openai:chat",
"gpt-5",
true,
None,
&auth_snapshot,
None,
None,
None,
true,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
true,
None,
)
.await;
cursor.mark_priority_page_emitted_for_tests();
cursor.ordering_config.scheduling_mode = SchedulerSchedulingMode::CacheAffinity;
assert!(!cursor.should_cache_current_priority_resolved_page());
cursor.client_session_affinity =
Some(aether_scheduler_core::ClientSessionAffinity::from_session_key("session-1"));
assert!(cursor.should_cache_current_priority_resolved_page());
cursor.ordering_config.scheduling_mode = SchedulerSchedulingMode::FixedOrder;
assert!(cursor.should_cache_current_priority_resolved_page());
cursor.ordering_config.scheduling_mode = SchedulerSchedulingMode::LoadBalance;
assert!(!cursor.should_cache_current_priority_resolved_page());
}
fn openai_responses_mapping_row() -> StoredMinimalCandidateSelectionRow {
StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-responses-mapped-1".to_string(),
@@ -1362,6 +1557,7 @@ mod tests {
None,
true,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
true,
None,
)
.await;
@@ -1421,6 +1617,7 @@ mod tests {
None,
true,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
true,
None,
)
.await;
@@ -1495,6 +1692,7 @@ mod tests {
None,
true,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
true,
None,
)
.await;
@@ -1,4 +1,5 @@
use std::collections::BTreeMap;
use std::time::Duration;
use aether_ai_serving::{run_ai_authenticated_decision_input, AiAuthenticatedDecisionInputPort};
use aether_routing_core::{
@@ -18,11 +19,15 @@ use crate::client_session_affinity::client_session_affinity_from_request;
use crate::clock::current_unix_secs;
use crate::routing::{
apply_routing_mutation_plan, build_routing_trace_seed, resolve_gateway_routing_policy,
select_gateway_routing_group, GatewayRoutingPolicyInput, GatewayRoutingSelectionError,
GatewayRoutingSelectionInput, ROUTING_GROUP_HEADER,
resolve_gateway_static_default_routing_policy, select_gateway_routing_group,
GatewayRoutingPolicyInput, GatewayRoutingSelectionError, GatewayRoutingSelectionInput,
GatewayStaticRoutingPolicyInput, ROUTING_GROUP_HEADER,
};
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AiExecutionDecision, AppState, GatewayError};
const ROUTING_GROUP_SELECTION_CACHE_TTL: Duration = Duration::from_secs(30);
#[derive(Debug, Clone)]
pub(crate) struct ResolvedLocalDecisionAuthInput {
pub(crate) auth_context: ExecutionRuntimeAuthContext,
@@ -221,6 +226,7 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
let explicit_group = routing_header_value_str(&parts.headers, ROUTING_GROUP_HEADER);
let selected_group = match state.routing_group_read_repository() {
Some(repository) => {
let user_groups_lookup_started_at = std::time::Instant::now();
let user_group_ids = match state
.list_user_groups_for_user(&input.auth_context.user_id)
.await
@@ -235,17 +241,50 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
Vec::new()
}
};
let selection = select_gateway_routing_group(
repository.as_ref(),
GatewayRoutingSelectionInput {
explicit_group: explicit_group.as_deref(),
user_id: Some(input.auth_context.user_id.as_str()),
api_key_id: Some(input.auth_context.api_key_id.as_str()),
user_group_ids: &user_group_ids,
},
)
.await
.map_err(routing_selection_error)?;
observe_gateway_stage_ms(
"routing_user_groups_lookup",
user_groups_lookup_started_at.elapsed().as_millis() as u64,
);
let selection_cache_key = routing_group_selection_cache_key(
explicit_group.as_deref(),
Some(input.auth_context.user_id.as_str()),
Some(input.auth_context.api_key_id.as_str()),
&user_group_ids,
);
let user_id = input.auth_context.user_id.clone();
let api_key_id = input.auth_context.api_key_id.clone();
let group_selection_started_at = std::time::Instant::now();
let selection = state
.routing_group_selection_cache
.get_or_load_once(
selection_cache_key,
ROUTING_GROUP_SELECTION_CACHE_TTL,
|| async move {
let selection_load_started_at = std::time::Instant::now();
let selection = select_gateway_routing_group(
repository.as_ref(),
GatewayRoutingSelectionInput {
explicit_group: explicit_group.as_deref(),
user_id: Some(user_id.as_str()),
api_key_id: Some(api_key_id.as_str()),
user_group_ids: &user_group_ids,
},
)
.await
.map_err(routing_selection_error)?;
observe_gateway_stage_ms(
"routing_group_selection_load",
selection_load_started_at.elapsed().as_millis() as u64,
);
Ok::<_, GatewayError>(Some(selection))
},
)
.await?
.unwrap_or_default();
observe_gateway_stage_ms(
"routing_group_selection",
group_selection_started_at.elapsed().as_millis() as u64,
);
selection.group.map(|group| {
(
Some(group.id),
@@ -279,7 +318,21 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
return Ok(());
};
if try_attach_static_default_routing_policy_to_input(
input,
parts,
body_json,
client_api_format,
group_id.as_deref(),
group_version,
&group_config_json,
selection_source.as_str(),
)? {
return Ok(());
}
let headers_json = headers_to_routing_value(&parts.headers);
let policy_resolve_started_at = std::time::Instant::now();
let policy = resolve_gateway_routing_policy(GatewayRoutingPolicyInput {
group_id: group_id.as_deref(),
group_version,
@@ -294,13 +347,22 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
body: body_json,
phase: RoutingRulePhase::ClientRequest,
})?;
observe_gateway_stage_ms(
"routing_policy_resolve",
policy_resolve_started_at.elapsed().as_millis() as u64,
);
let mut effective_body_json = body_json.clone();
let mut effective_headers = parts.headers.clone();
let mutation_apply_started_at = std::time::Instant::now();
apply_routing_mutation_plan(
&mut effective_body_json,
&mut effective_headers,
&policy.mutation_plan,
)?;
observe_gateway_stage_ms(
"routing_mutation_apply",
mutation_apply_started_at.elapsed().as_millis() as u64,
);
let mut requested_model_changed = false;
if let Some(mut mutated_model) = extract_standard_requested_model(&effective_body_json) {
@@ -324,6 +386,7 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
let effective_headers_json = headers_to_routing_value(&effective_headers);
input.client_session_affinity =
client_session_affinity_from_request(&effective_headers, Some(&effective_body_json));
let final_policy_resolve_started_at = std::time::Instant::now();
let mut final_policy = resolve_gateway_routing_policy(GatewayRoutingPolicyInput {
group_id: group_id.as_deref(),
group_version,
@@ -338,6 +401,10 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
body: &effective_body_json,
phase: RoutingRulePhase::ClientRequest,
})?;
observe_gateway_stage_ms(
"routing_policy_resolve",
final_policy_resolve_started_at.elapsed().as_millis() as u64,
);
final_policy.mutation_plan = policy.mutation_plan.clone();
input.routing_trace_seed = Some(build_routing_trace_seed(&final_policy, client_api_format));
input.routing_policy = Some(final_policy);
@@ -353,6 +420,46 @@ pub(crate) async fn attach_routing_policy_to_local_requested_model_input(
Ok(())
}
fn try_attach_static_default_routing_policy_to_input(
input: &mut LocalRequestedModelDecisionInput,
parts: &http::request::Parts,
body_json: &Value,
client_api_format: &str,
group_id: Option<&str>,
group_version: Option<i64>,
group_config_json: &Value,
selection_source: &str,
) -> Result<bool, GatewayError> {
let static_policy_resolve_started_at = std::time::Instant::now();
let Some(policy) =
resolve_gateway_static_default_routing_policy(GatewayStaticRoutingPolicyInput {
group_id,
group_version,
group_config_json,
selection_source,
requested_model: input.requested_model.as_str(),
resolved_model: input.requested_model.as_str(),
})?
else {
observe_gateway_stage_ms(
"routing_static_policy_resolve",
static_policy_resolve_started_at.elapsed().as_millis() as u64,
);
return Ok(false);
};
observe_gateway_stage_ms(
"routing_static_policy_resolve",
static_policy_resolve_started_at.elapsed().as_millis() as u64,
);
input.client_session_affinity =
client_session_affinity_from_request(&parts.headers, Some(body_json));
input.routing_trace_seed = Some(build_routing_trace_seed(&policy, client_api_format));
input.routing_policy = Some(policy);
input.routing_context = None;
Ok(true)
}
pub(crate) fn build_local_authenticated_decision_input(
resolved_input: ResolvedLocalDecisionAuthInput,
) -> LocalAuthenticatedDecisionInput {
@@ -410,6 +517,33 @@ fn routing_header_value_str(headers: &http::HeaderMap, key: &str) -> Option<Stri
.map(ToOwned::to_owned)
}
fn routing_group_selection_cache_key(
explicit_group: Option<&str>,
user_id: Option<&str>,
api_key_id: Option<&str>,
user_group_ids: &[String],
) -> String {
let groups = user_group_ids
.iter()
.map(|value| escape_cache_key_part(value))
.collect::<Vec<_>>()
.join(",");
format!(
"v1|explicit={}|user={}|api_key={}|groups={}",
escape_cache_key_part(explicit_group.unwrap_or_default()),
escape_cache_key_part(user_id.unwrap_or_default()),
escape_cache_key_part(api_key_id.unwrap_or_default()),
groups
)
}
fn escape_cache_key_part(value: &str) -> String {
value
.replace('%', "%25")
.replace('|', "%7C")
.replace(',', "%2C")
}
fn btree_headers_to_header_map(
headers: &BTreeMap<String, String>,
) -> Result<HeaderMap, GatewayError> {
@@ -754,6 +888,119 @@ mod tests {
.group_config_json = config;
}
#[test]
fn static_default_routing_policy_attaches_without_request_context() {
let request = http::Request::builder()
.header("content-type", "application/json")
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let mut input = LocalRequestedModelDecisionInput {
auth_context: sample_auth_context(),
requested_model: "mock-model".to_string(),
auth_snapshot: sample_auth_snapshot(),
required_capabilities: None,
request_auth_channel: None,
client_session_affinity: None,
routing_policy: None,
routing_trace_seed: None,
routing_context: Some(LocalRoutingRequestContext {
group_id: Some("stale".to_string()),
group_version: Some(1),
group_config_json: json!({}),
selection_source: "stale".to_string(),
client_api_format: "openai:chat".to_string(),
effective_body_json: json!({}),
effective_headers: HeaderMap::new(),
}),
};
let group_config_json = json!({
"default_policy": {
"priority_mode": "global_key",
"scheduling_mode": "load_balance",
"keep_priority_on_conversion": true
},
"allowed_models": [],
"model_policies": [],
"rules": []
});
let attached = try_attach_static_default_routing_policy_to_input(
&mut input,
&parts,
&json!({"model": "mock-model"}),
"openai:chat",
Some("group-1"),
Some(4),
&group_config_json,
"system_default",
)
.expect("static routing should attach");
assert!(attached);
assert!(input.routing_context.is_none());
let policy = input.routing_policy.as_ref().expect("policy should be set");
assert_eq!(policy.group_id.as_deref(), Some("group-1"));
assert_eq!(policy.group_version, Some(4));
assert_eq!(
policy.priority_mode,
aether_routing_core::RoutingSetPriorityMode::GlobalKey
);
assert_eq!(
policy.scheduling_mode,
aether_routing_core::RoutingSchedulingMode::LoadBalance
);
assert!(policy.keep_priority_on_conversion);
assert!(policy.mutation_plan.is_empty());
assert!(input.routing_trace_seed.is_some());
}
#[test]
fn dynamic_routing_policy_does_not_attach_static_fast_path() {
let request = http::Request::builder()
.body(())
.expect("request should build");
let (parts, _) = request.into_parts();
let mut input = LocalRequestedModelDecisionInput {
auth_context: sample_auth_context(),
requested_model: "mock-model".to_string(),
auth_snapshot: sample_auth_snapshot(),
required_capabilities: None,
request_auth_channel: None,
client_session_affinity: None,
routing_policy: None,
routing_trace_seed: None,
routing_context: None,
};
let group_config_json = json!({
"rules": [{
"id": "rule-1",
"conditions": {},
"actions": [{
"type": "restrict_providers",
"provider_ids": ["provider-1"]
}]
}]
});
let attached = try_attach_static_default_routing_policy_to_input(
&mut input,
&parts,
&json!({"model": "mock-model"}),
"openai:chat",
Some("group-1"),
Some(4),
&group_config_json,
"system_default",
)
.expect("dynamic config should not fail static detection");
assert!(!attached);
assert!(input.routing_policy.is_none());
assert!(input.routing_trace_seed.is_none());
assert!(input.routing_context.is_none());
}
#[test]
fn provider_request_routing_policy_mutates_decision_body_headers_and_report_context() {
let input = sample_decision_input();
@@ -77,6 +77,7 @@ pub(crate) async fn resolve_provider_chat_pii_redaction<'a>(
};
let request_cache_key = request_redaction_cache_key(format, body_json);
if let Some(cached) = slot.cached_request_redaction(&request_cache_key) {
crate::stage_metrics::record_chat_pii_redaction_request_cache_hit();
observe_gateway_stage_ms("chat_pii_redaction_request_cache_hit", 0);
return Ok(provider_redaction_from_cached(
slot,
@@ -85,6 +86,7 @@ pub(crate) async fn resolve_provider_chat_pii_redaction<'a>(
cached,
));
}
crate::stage_metrics::record_chat_pii_redaction_request_cache_miss();
let runtime_config_started_at = Instant::now();
let runtime_config = read_chat_pii_redaction_runtime_config(state)
@@ -6,6 +6,7 @@ mod request;
mod support;
pub(super) use self::payload::maybe_build_local_openai_chat_decision_payload_for_candidate;
pub(super) use self::request::LocalOpenAiChatRequestPreparation;
pub(super) use self::support::{
build_lazy_local_openai_chat_candidate_attempt_source,
build_local_openai_chat_candidate_attempt_source,
@@ -18,7 +18,9 @@ use crate::{
AiExecutionDecision, AppState, GatewayError,
};
use super::request::resolve_local_openai_chat_candidate_payload_parts;
use super::request::{
resolve_local_openai_chat_candidate_payload_parts, LocalOpenAiChatRequestPreparation,
};
use super::support::{LocalOpenAiChatCandidateAttempt, LocalOpenAiChatDecisionInput};
#[allow(clippy::too_many_arguments)]
@@ -28,6 +30,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
preparation: Option<&mut LocalOpenAiChatRequestPreparation>,
attempt: LocalOpenAiChatCandidateAttempt,
decision_kind: &str,
report_kind: &str,
@@ -48,6 +51,7 @@ pub(crate) async fn maybe_build_local_openai_chat_decision_payload_for_candidate
trace_id,
body_json,
input,
preparation,
&eligible,
candidate_index,
&candidate_id,
@@ -82,6 +82,39 @@ pub(crate) struct LocalOpenAiChatCandidatePayloadParts {
pub(super) image_request_summary: Option<Value>,
}
#[derive(Default)]
pub(crate) struct LocalOpenAiChatRequestPreparation {
model_directives_enabled: BTreeMap<(String, String), bool>,
}
impl LocalOpenAiChatRequestPreparation {
async fn model_directives_enabled(
&mut self,
state: &AppState,
provider_api_format: &str,
requested_model: &str,
) -> bool {
let key = (
provider_api_format.trim().to_ascii_lowercase(),
requested_model.trim().to_string(),
);
if let Some(enabled) = self.model_directives_enabled.get(&key) {
crate::stage_metrics::record_openai_chat_model_directive_cache_hit();
return *enabled;
}
crate::stage_metrics::record_openai_chat_model_directive_cache_miss();
let enabled =
crate::system_features::reasoning_model_directive_enabled_for_api_format_and_model(
state,
provider_api_format,
Some(requested_model),
)
.await;
self.model_directives_enabled.insert(key, enabled);
enabled
}
}
fn is_grok_text_provider_api_format(provider_api_format: &str) -> bool {
matches!(
crate::ai_serving::normalize_api_format_alias(provider_api_format).as_str(),
@@ -96,6 +129,7 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
trace_id: &str,
body_json: &serde_json::Value,
input: &LocalOpenAiChatDecisionInput,
mut preparation: Option<&mut LocalOpenAiChatRequestPreparation>,
eligible: &EligibleLocalExecutionCandidate,
candidate_index: u32,
candidate_id: &str,
@@ -112,13 +146,18 @@ pub(crate) async fn resolve_local_openai_chat_candidate_payload_parts(
let force_body_stream_field =
endpoint_config_forces_body_stream_field(transport.endpoint.config.as_ref());
let model_directives_started_at = std::time::Instant::now();
let enable_model_directives =
let enable_model_directives = if let Some(preparation) = preparation {
preparation
.model_directives_enabled(state, provider_api_format, &input.requested_model)
.await
} else {
crate::system_features::reasoning_model_directive_enabled_for_api_format_and_model(
state,
provider_api_format,
Some(&input.requested_model),
)
.await;
.await
};
observe_gateway_stage_ms(
"openai_chat_payload_model_directives",
model_directives_started_at.elapsed().as_millis() as u64,
@@ -1907,6 +1946,7 @@ mod tests {
"trace-openai-chat-gemini-cli",
&body_json,
&sample_input(),
None,
&sample_gemini_cli_eligible(),
0,
"candidate-0",
@@ -13,6 +13,7 @@ use self::decision::{
build_lazy_local_openai_chat_candidate_attempt_source,
maybe_build_local_openai_chat_decision_payload_for_candidate, LocalOpenAiChatCandidateAttempt,
LocalOpenAiChatCandidateAttemptSource, LocalOpenAiChatDecisionInput,
LocalOpenAiChatRequestPreparation,
};
use self::plans::{
build_local_openai_chat_stream_attempt_source, build_local_openai_chat_stream_plan_and_reports,
@@ -159,6 +160,7 @@ pub(crate) async fn maybe_build_sync_local_decision_payload(
trace_id,
body_json,
&input,
None,
attempt,
OPENAI_CHAT_SYNC_PLAN_KIND,
"openai_chat_sync_success",
@@ -211,6 +213,7 @@ pub(crate) async fn maybe_build_stream_local_decision_payload(
trace_id,
body_json,
&input,
None,
attempt,
OPENAI_CHAT_STREAM_PLAN_KIND,
"openai_chat_stream_success",
@@ -9,6 +9,7 @@ use crate::ai_serving::planner::decision_input::{
};
use crate::ai_serving::resolve_local_decision_execution_runtime_auth_context;
use crate::client_session_affinity::client_session_affinity_from_parts;
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AppState, GatewayError};
pub(crate) async fn resolve_local_openai_chat_decision_input(
@@ -59,6 +60,7 @@ pub(crate) async fn resolve_local_openai_chat_decision_input(
return Ok(None);
};
let auth_started_at = std::time::Instant::now();
let resolved_input = match resolve_local_authenticated_decision_input(
state,
auth_context.clone(),
@@ -106,10 +108,20 @@ pub(crate) async fn resolve_local_openai_chat_decision_input(
return Err(err);
}
};
observe_gateway_stage_ms(
"openai_chat_decision_input_auth",
auth_started_at.elapsed().as_millis() as u64,
);
let mut input = build_local_requested_model_decision_input(resolved_input, requested_model);
input.request_auth_channel = decision.request_auth_channel.clone();
let affinity_started_at = std::time::Instant::now();
input.client_session_affinity = client_session_affinity_from_parts(parts, Some(body_json));
observe_gateway_stage_ms(
"openai_chat_decision_input_affinity",
affinity_started_at.elapsed().as_millis() as u64,
);
let routing_started_at = std::time::Instant::now();
if let Err(err) = attach_routing_policy_to_local_requested_model_input(
state,
parts,
@@ -126,5 +138,9 @@ pub(crate) async fn resolve_local_openai_chat_decision_input(
);
return Err(err);
}
observe_gateway_stage_ms(
"openai_chat_decision_input_routing",
routing_started_at.elapsed().as_millis() as u64,
);
Ok(Some(input))
}
@@ -1,11 +1,12 @@
use async_trait::async_trait;
use std::collections::VecDeque;
use tracing::warn;
use super::super::{
build_lazy_local_openai_chat_candidate_attempt_source,
maybe_build_local_openai_chat_decision_payload_for_candidate, AppState, GatewayControlDecision,
GatewayError, LocalOpenAiChatCandidateAttempt, LocalOpenAiChatCandidateAttemptSource,
LocalOpenAiChatDecisionInput,
LocalOpenAiChatDecisionInput, LocalOpenAiChatRequestPreparation,
};
use super::diagnostic::{
set_local_openai_chat_candidate_evaluation_diagnostic, set_local_openai_chat_miss_diagnostic,
@@ -18,7 +19,23 @@ use crate::ai_serving::planner::plan_builders::{
build_openai_chat_stream_plan_from_decision, AiStreamAttempt,
};
use crate::ai_serving::planner::runtime_miss::apply_local_runtime_candidate_terminal_reason;
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::ai_serving::planner::standard::build_local_openai_chat_upstream_url;
use crate::ai_serving::transport::{
is_windsurf_provider_transport, local_openai_chat_transport_unsupported_reason,
};
use crate::clock::request_distribution_seed;
use crate::stage_metrics::{
observe_gateway_stage_ms, record_openai_chat_stream_payload_build_prefetch_avoided,
record_openai_chat_stream_payload_build_selected,
record_openai_chat_stream_raw_candidates_scanned,
record_openai_chat_stream_target_select_selected_rank,
};
use crate::upstream_admission::upstream_target_key_from_url;
const OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW_ENV: &str =
"AETHER_GATEWAY_OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW";
const DEFAULT_OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW: usize = 2;
const MAX_OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW: usize = 8;
pub(crate) struct LocalOpenAiChatStreamAttemptSource<'a> {
state: &'a AppState,
@@ -27,6 +44,8 @@ pub(crate) struct LocalOpenAiChatStreamAttemptSource<'a> {
body_json: serde_json::Value,
input: LocalOpenAiChatDecisionInput,
candidates: LocalOpenAiChatCandidateAttemptSource<'a>,
prefetched_attempts: VecDeque<LocalOpenAiChatCandidateAttempt>,
request_preparation: LocalOpenAiChatRequestPreparation,
}
pub(crate) async fn build_local_openai_chat_stream_attempt_source<'a>(
@@ -41,6 +60,7 @@ pub(crate) async fn build_local_openai_chat_stream_attempt_source<'a>(
return Ok(None);
}
let attempt_source_started_at = std::time::Instant::now();
let Some(input) = resolve_local_openai_chat_decision_input(
state, parts, trace_id, decision, body_json, plan_kind, true,
)
@@ -77,6 +97,10 @@ pub(crate) async fn build_local_openai_chat_stream_attempt_source<'a>(
Some(input.requested_model.as_str()),
candidate_count,
);
observe_gateway_stage_ms(
"openai_chat_attempt_source_build",
attempt_source_started_at.elapsed().as_millis() as u64,
);
Ok(Some((
LocalOpenAiChatStreamAttemptSource {
@@ -86,6 +110,8 @@ pub(crate) async fn build_local_openai_chat_stream_attempt_source<'a>(
body_json: effective_body_json,
input,
candidates,
prefetched_attempts: VecDeque::new(),
request_preparation: LocalOpenAiChatRequestPreparation::default(),
},
candidate_count,
)))
@@ -94,21 +120,42 @@ pub(crate) async fn build_local_openai_chat_stream_attempt_source<'a>(
#[async_trait]
impl LocalExecutionAttemptSource<AiStreamAttempt> for LocalOpenAiChatStreamAttemptSource<'_> {
async fn next_execution_attempt(&mut self) -> Result<Option<AiStreamAttempt>, GatewayError> {
let select_started_at = std::time::Instant::now();
let selected = self.next_execution_attempt_with_target_select().await?;
observe_gateway_stage_ms(
"openai_chat_stream_target_select",
select_started_at.elapsed().as_millis() as u64,
);
Ok(selected)
}
async fn drain_execution_attempts(&mut self) -> Result<Vec<AiStreamAttempt>, GatewayError> {
let mut drained = Vec::new();
for attempt in self.candidates.drain_static_attempts() {
if let Some(attempt) = self.build_stream_attempt(attempt).await? {
drained.push(attempt);
}
}
Ok(drained)
}
}
impl LocalOpenAiChatStreamAttemptSource<'_> {
async fn next_execution_attempt_with_target_select(
&mut self,
) -> Result<Option<AiStreamAttempt>, GatewayError> {
loop {
let source_started_at = std::time::Instant::now();
let Some(attempt) = self.candidates.next_attempt().await? else {
observe_gateway_stage_ms(
"stream_candidate_source_next",
source_started_at.elapsed().as_millis() as u64,
let Some(attempt) = self.next_raw_attempt_with_target_select().await? else {
apply_local_runtime_candidate_terminal_reason(
self.state,
self.trace_id,
"no_local_stream_plans",
);
break;
return Ok(None);
};
observe_gateway_stage_ms(
"stream_candidate_source_next",
source_started_at.elapsed().as_millis() as u64,
);
let plan_started_at = std::time::Instant::now();
record_openai_chat_stream_payload_build_selected();
match self.build_stream_attempt(attempt).await? {
Some(attempt) => {
observe_gateway_stage_ms(
@@ -126,28 +173,122 @@ impl LocalExecutionAttemptSource<AiStreamAttempt> for LocalOpenAiChatStreamAttem
}
}
}
apply_local_runtime_candidate_terminal_reason(
self.state,
self.trace_id,
"no_local_stream_plans",
);
Ok(None)
}
async fn drain_execution_attempts(&mut self) -> Result<Vec<AiStreamAttempt>, GatewayError> {
let mut drained = Vec::new();
for attempt in self.candidates.drain_static_attempts() {
if let Some(attempt) = self.build_stream_attempt(attempt).await? {
drained.push(attempt);
async fn next_raw_attempt_with_target_select(
&mut self,
) -> Result<Option<LocalOpenAiChatCandidateAttempt>, GatewayError> {
let select_window = openai_chat_stream_target_select_window();
if select_window <= 1 {
return self.next_raw_attempt_linear().await;
}
let mut attempts = Vec::with_capacity(select_window);
for _ in 0..select_window {
match self.next_raw_attempt_linear().await? {
Some(attempt) => attempts.push(attempt),
None => break,
}
}
Ok(drained)
if attempts.is_empty() {
return Ok(None);
}
record_openai_chat_stream_raw_candidates_scanned(attempts.len());
let seed = request_distribution_seed();
let target_keys = attempts
.iter()
.map(|attempt| self.lightweight_target_key_for_attempt(attempt))
.collect::<Vec<_>>();
for target_key in target_keys.iter().flatten() {
self.state
.upstream_target_admission
.record_raw_seen_for_target_key(target_key);
}
let selected_index = if target_keys.iter().all(Option::is_some) {
let choices = attempts
.iter()
.zip(target_keys.iter())
.map(|(attempt, target_key)| {
let target_key = target_key.as_deref().unwrap_or("-");
let snapshot = self
.state
.upstream_target_admission
.snapshot_for_target_key(target_key);
TargetSelectChoice {
target_key,
identity: target_select_candidate_identity(attempt),
in_flight: snapshot
.as_ref()
.map(|snapshot| snapshot.in_flight)
.unwrap_or(0),
selection_pressure_total: snapshot
.as_ref()
.map(|snapshot| snapshot.selection_pressure_total)
.unwrap_or(0),
}
})
.collect::<Vec<_>>();
select_target_index(seed, &choices)
} else {
0
};
record_openai_chat_stream_target_select_selected_rank(selected_index);
record_openai_chat_stream_payload_build_prefetch_avoided(attempts.len().saturating_sub(1));
if let Some(Some(target_key)) = target_keys.get(selected_index) {
self.state
.upstream_target_admission
.record_preselect_for_target_key(target_key);
}
let selected = attempts.remove(selected_index);
self.prefetched_attempts.extend(attempts);
Ok(Some(selected))
}
}
impl LocalOpenAiChatStreamAttemptSource<'_> {
async fn build_stream_attempt(
async fn next_raw_attempt_linear(
&mut self,
) -> Result<Option<LocalOpenAiChatCandidateAttempt>, GatewayError> {
if let Some(attempt) = self.prefetched_attempts.pop_front() {
return Ok(Some(attempt));
}
let source_started_at = std::time::Instant::now();
let attempt = self.candidates.next_attempt().await?;
observe_gateway_stage_ms(
"stream_candidate_source_next",
source_started_at.elapsed().as_millis() as u64,
);
Ok(attempt)
}
fn lightweight_target_key_for_attempt(
&self,
attempt: &LocalOpenAiChatCandidateAttempt,
) -> Option<String> {
let provider_api_format = attempt.eligible.provider_api_format.trim();
if !provider_api_format.eq_ignore_ascii_case("openai:chat") {
return None;
}
let transport = &attempt.eligible.transport;
if transport
.provider
.provider_type
.trim()
.eq_ignore_ascii_case("grok")
|| is_windsurf_provider_transport(transport)
|| local_openai_chat_transport_unsupported_reason(transport).is_some()
{
return None;
}
if transport.provider.proxy.is_some()
|| transport.endpoint.proxy.is_some()
|| transport.key.proxy.is_some()
{
return None;
}
let upstream_url = build_local_openai_chat_upstream_url(self.parts, transport)?;
upstream_target_key_from_url(upstream_url.as_str(), None)
}
async fn build_stream_attempt(
&mut self,
attempt: LocalOpenAiChatCandidateAttempt,
) -> Result<Option<AiStreamAttempt>, GatewayError> {
let upstream_is_stream = openai_chat_upstream_is_stream_for_candidate(
@@ -161,6 +302,7 @@ impl LocalOpenAiChatStreamAttemptSource<'_> {
self.trace_id,
&self.body_json,
&self.input,
Some(&mut self.request_preparation),
attempt,
OPENAI_CHAT_STREAM_PLAN_KIND,
"openai_chat_stream_success",
@@ -185,6 +327,97 @@ impl LocalOpenAiChatStreamAttemptSource<'_> {
}
}
fn openai_chat_stream_target_select_window() -> usize {
std::env::var(OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW_ENV)
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|value| *value > 0)
.unwrap_or(DEFAULT_OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW)
.clamp(1, MAX_OPENAI_CHAT_STREAM_TARGET_SELECT_WINDOW)
}
#[derive(Clone, Copy)]
struct TargetSelectCandidateIdentity<'a> {
provider_id: &'a str,
endpoint_id: &'a str,
key_id: &'a str,
candidate_id: &'a str,
}
#[derive(Clone, Copy)]
struct TargetSelectChoice<'a> {
target_key: &'a str,
identity: TargetSelectCandidateIdentity<'a>,
in_flight: usize,
selection_pressure_total: u64,
}
fn select_target_index(seed: u64, choices: &[TargetSelectChoice<'_>]) -> usize {
choices
.iter()
.enumerate()
.min_by_key(|(index, choice)| {
target_select_score(
seed,
choice.target_key,
&choice.identity,
*index,
choice.in_flight,
choice.selection_pressure_total,
)
})
.map(|(index, _)| index)
.unwrap_or(0)
}
fn target_select_candidate_identity(
attempt: &LocalOpenAiChatCandidateAttempt,
) -> TargetSelectCandidateIdentity<'_> {
TargetSelectCandidateIdentity {
provider_id: &attempt.eligible.candidate.provider_id,
endpoint_id: &attempt.eligible.candidate.endpoint_id,
key_id: &attempt.eligible.candidate.key_id,
candidate_id: &attempt.candidate_id,
}
}
fn target_select_tie_break(
seed: u64,
target_key: &str,
identity: &TargetSelectCandidateIdentity<'_>,
index: usize,
) -> u64 {
let mut hash = seed ^ ((index as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
hash = hash_string(hash, target_key);
hash = hash_string(hash, identity.provider_id);
hash = hash_string(hash, identity.endpoint_id);
hash = hash_string(hash, identity.key_id);
hash_string(hash, identity.candidate_id)
}
fn hash_string(mut hash: u64, value: &str) -> u64 {
for byte in value.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100_0000_01B3);
}
hash
}
fn target_select_score(
seed: u64,
target_key: &str,
identity: &TargetSelectCandidateIdentity<'_>,
index: usize,
in_flight: usize,
selected_total: u64,
) -> (usize, u64, u64) {
(
in_flight,
selected_total,
target_select_tie_break(seed, target_key, identity, index),
)
}
pub(crate) async fn build_local_openai_chat_stream_plan_and_reports(
state: &AppState,
parts: &http::request::Parts,
@@ -234,3 +467,82 @@ pub(crate) async fn build_local_openai_chat_stream_plan_and_reports(
Ok(plans)
}
#[cfg(test)]
mod tests {
use super::*;
fn identity<'a>(
endpoint_id: &'a str,
candidate_id: &'a str,
) -> TargetSelectCandidateIdentity<'a> {
TargetSelectCandidateIdentity {
provider_id: "provider",
endpoint_id,
key_id: "key",
candidate_id,
}
}
#[test]
fn target_select_score_prefers_lower_in_flight() {
let busy = identity("endpoint-a", "candidate-a");
let idle = identity("endpoint-b", "candidate-b");
assert!(
target_select_score(7, "http://127.0.0.1:18182|proxy=-", &idle, 1, 0, 10)
< target_select_score(7, "http://127.0.0.1:18181|proxy=-", &busy, 0, 5, 0)
);
}
#[test]
fn target_select_tie_break_distinguishes_equivalent_targets() {
let left = identity("endpoint-a", "candidate-a");
let right = identity("endpoint-b", "candidate-b");
assert_ne!(
target_select_tie_break(11, "http://127.0.0.1:18181|proxy=-", &left, 0),
target_select_tie_break(11, "http://127.0.0.1:18182|proxy=-", &right, 1)
);
}
#[test]
fn select_target_index_prefers_lower_in_flight_target() {
let choices = [
TargetSelectChoice {
target_key: "http://127.0.0.1:18181|proxy=-",
identity: identity("endpoint-a", "candidate-a"),
in_flight: 8,
selection_pressure_total: 0,
},
TargetSelectChoice {
target_key: "http://127.0.0.1:18182|proxy=-",
identity: identity("endpoint-b", "candidate-b"),
in_flight: 1,
selection_pressure_total: 100,
},
];
assert_eq!(select_target_index(17, &choices), 1);
}
#[test]
fn select_target_index_uses_selection_pressure_before_tie_break() {
let choices = [
TargetSelectChoice {
target_key: "http://127.0.0.1:18181|proxy=-",
identity: identity("endpoint-a", "candidate-a"),
in_flight: 0,
selection_pressure_total: 20,
},
TargetSelectChoice {
target_key: "http://127.0.0.1:18182|proxy=-",
identity: identity("endpoint-b", "candidate-b"),
in_flight: 0,
selection_pressure_total: 1,
},
];
assert_eq!(select_target_index(19, &choices), 1);
}
}
@@ -134,6 +134,7 @@ impl LocalOpenAiChatSyncAttemptSource<'_> {
self.trace_id,
&self.body_json,
&self.input,
None,
attempt,
OPENAI_CHAT_SYNC_PLAN_KIND,
"openai_chat_sync_success",
+154 -2
View File
@@ -17,11 +17,16 @@ use sha2::Digest as _;
use crate::ai_serving::{
EligibleLocalExecutionCandidate, GatewayAuthApiKeySnapshot, SkippedLocalExecutionCandidate,
};
use crate::data::candidate_selection::RequestedModelCandidateRowsPage;
const DEFAULT_CANDIDATE_PAGE_CACHE_TTL_MS: u64 = 250;
const MIN_CANDIDATE_PAGE_CACHE_TTL_MS: u64 = 50;
const MAX_CANDIDATE_PAGE_CACHE_TTL_MS: u64 = 1_000;
const CANDIDATE_PAGE_CACHE_TTL_ENV: &str = "AETHER_GATEWAY_CANDIDATE_PAGE_CACHE_TTL_MS";
const DEFAULT_CANDIDATE_PAGE_CACHE_STALE_TTL_MS: u64 = 300_000;
const MIN_CANDIDATE_PAGE_CACHE_STALE_TTL_MS: u64 = 1_000;
const MAX_CANDIDATE_PAGE_CACHE_STALE_TTL_MS: u64 = 300_000;
const CANDIDATE_PAGE_CACHE_STALE_TTL_ENV: &str = "AETHER_GATEWAY_CANDIDATE_PAGE_CACHE_STALE_TTL_MS";
pub(crate) type CandidatePageSnapshot = AiCandidatePreselectionOutcome<
SchedulerMinimalCandidateSelectionCandidate,
@@ -31,6 +36,9 @@ pub(crate) type CandidatePageSnapshot = AiCandidatePreselectionOutcome<
pub(crate) type CandidatePageCache =
super::ValueCache<CandidatePageCacheKey, Arc<CandidatePageSnapshot>>;
pub(crate) type CandidateRowPageCache =
super::ValueCache<CandidateRowPageCacheKey, Arc<RequestedModelCandidateRowsPage>>;
#[derive(Debug, Clone)]
pub(crate) struct CandidateResolvedPageSnapshot {
pub(crate) candidates: Vec<EligibleLocalExecutionCandidate>,
@@ -54,6 +62,21 @@ struct CandidatePageCacheMetrics {
resolve_load_total: AtomicU64,
resolve_follower_wait_total: AtomicU64,
resolve_miss_total: AtomicU64,
row_hit_total: AtomicU64,
row_load_total: AtomicU64,
row_follower_wait_total: AtomicU64,
row_miss_total: AtomicU64,
row_none_total: AtomicU64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CandidateRowPageCacheKey {
api_format: String,
requested_model_name: String,
requested_name: String,
offset: u32,
limit: u32,
enable_model_directives: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -83,6 +106,26 @@ pub(crate) struct CandidateResolvedPageCacheKey {
resolution_mode: &'static str,
}
impl CandidateRowPageCacheKey {
pub(crate) fn new(
api_format: &str,
requested_model_name: &str,
requested_name: &str,
offset: u32,
limit: u32,
enable_model_directives: bool,
) -> Self {
Self {
api_format: normalize_api_format(api_format),
requested_model_name: normalize_text_key(requested_model_name),
requested_name: normalize_text_key(requested_name),
offset,
limit,
enable_model_directives,
}
}
}
impl CandidatePageCacheKey {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
@@ -177,8 +220,16 @@ pub(crate) fn candidate_page_cache_ttl_from_env() -> Duration {
}
pub(crate) fn candidate_page_cache_stale_ttl(ttl: Duration) -> Duration {
let stale_ttl = ttl.saturating_mul(8);
stale_ttl.min(Duration::from_secs(2)).max(ttl)
let stale_ttl_ms = std::env::var(CANDIDATE_PAGE_CACHE_STALE_TTL_ENV)
.ok()
.and_then(|value| value.trim().parse::<u64>().ok())
.filter(|value| *value > 0)
.unwrap_or(DEFAULT_CANDIDATE_PAGE_CACHE_STALE_TTL_MS)
.clamp(
MIN_CANDIDATE_PAGE_CACHE_STALE_TTL_MS,
MAX_CANDIDATE_PAGE_CACHE_STALE_TTL_MS,
);
Duration::from_millis(stale_ttl_ms).max(ttl)
}
pub(crate) fn record_candidate_page_cache_hit() {
@@ -235,6 +286,36 @@ pub(crate) fn record_candidate_page_resolve_cache_follower_wait() {
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_candidate_row_page_cache_hit() {
CANDIDATE_PAGE_CACHE_METRICS
.row_hit_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_candidate_row_page_cache_miss() {
CANDIDATE_PAGE_CACHE_METRICS
.row_miss_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_candidate_row_page_cache_load() {
CANDIDATE_PAGE_CACHE_METRICS
.row_load_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_candidate_row_page_cache_follower_wait() {
CANDIDATE_PAGE_CACHE_METRICS
.row_follower_wait_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_candidate_row_page_cache_none() {
CANDIDATE_PAGE_CACHE_METRICS
.row_none_total
.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn candidate_page_cache_metric_samples() -> Vec<MetricSample> {
vec![
MetricSample::new(
@@ -309,6 +390,46 @@ pub(crate) fn candidate_page_cache_metric_samples() -> Vec<MetricSample> {
.resolve_follower_wait_total
.load(Ordering::Relaxed),
),
MetricSample::new(
"candidate_row_page_cache_hit_total",
"Total candidate DB row page cache hits.",
MetricKind::Counter,
CANDIDATE_PAGE_CACHE_METRICS
.row_hit_total
.load(Ordering::Relaxed),
),
MetricSample::new(
"candidate_row_page_cache_miss_total",
"Total candidate DB row page cache misses before singleflight registration.",
MetricKind::Counter,
CANDIDATE_PAGE_CACHE_METRICS
.row_miss_total
.load(Ordering::Relaxed),
),
MetricSample::new(
"candidate_row_page_cache_load_total",
"Total candidate DB row page cache loader executions.",
MetricKind::Counter,
CANDIDATE_PAGE_CACHE_METRICS
.row_load_total
.load(Ordering::Relaxed),
),
MetricSample::new(
"candidate_row_page_cache_follower_wait_total",
"Total candidate DB row page cache requests that waited for another loader.",
MetricKind::Counter,
CANDIDATE_PAGE_CACHE_METRICS
.row_follower_wait_total
.load(Ordering::Relaxed),
),
MetricSample::new(
"candidate_row_page_cache_none_total",
"Total candidate DB row page cache lookups that resolved to no rows.",
MetricKind::Counter,
CANDIDATE_PAGE_CACHE_METRICS
.row_none_total
.load(Ordering::Relaxed),
),
]
}
@@ -371,6 +492,9 @@ mod tests {
use super::*;
use aether_data::repository::auth::ResolvedAuthApiKeySnapshot;
use serde_json::json;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn auth_snapshot(user_id: &str, api_key_id: &str) -> ResolvedAuthApiKeySnapshot {
ResolvedAuthApiKeySnapshot {
@@ -401,6 +525,34 @@ mod tests {
}
}
#[test]
fn candidate_page_cache_stale_ttl_defaults_longer_than_burst_gap() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
std::env::remove_var(CANDIDATE_PAGE_CACHE_STALE_TTL_ENV);
assert_eq!(
candidate_page_cache_stale_ttl(Duration::from_millis(250)),
Duration::from_millis(DEFAULT_CANDIDATE_PAGE_CACHE_STALE_TTL_MS)
);
}
#[test]
fn candidate_page_cache_stale_ttl_respects_env_and_never_under_fresh_ttl() {
let _guard = ENV_LOCK.lock().expect("env lock poisoned");
std::env::set_var(CANDIDATE_PAGE_CACHE_STALE_TTL_ENV, "100");
assert_eq!(
candidate_page_cache_stale_ttl(Duration::from_millis(2_500)),
Duration::from_millis(2_500)
);
std::env::set_var(CANDIDATE_PAGE_CACHE_STALE_TTL_ENV, "60000");
assert_eq!(
candidate_page_cache_stale_ttl(Duration::from_millis(250)),
Duration::from_secs(60)
);
std::env::remove_var(CANDIDATE_PAGE_CACHE_STALE_TTL_ENV);
}
#[test]
fn candidate_page_cache_key_isolates_auth_model_format_and_capabilities() {
let auth_a = auth_snapshot("user-a", "key-a");
+5 -2
View File
@@ -20,8 +20,11 @@ pub(crate) use candidate_page::{
record_candidate_page_cache_miss, record_candidate_page_cache_none,
record_candidate_page_resolve_cache_follower_wait, record_candidate_page_resolve_cache_hit,
record_candidate_page_resolve_cache_load, record_candidate_page_resolve_cache_miss,
CandidatePageCache, CandidatePageCacheKey, CandidatePageSnapshot, CandidateResolvedPageCache,
CandidateResolvedPageCacheKey, CandidateResolvedPageSnapshot,
record_candidate_row_page_cache_follower_wait, record_candidate_row_page_cache_hit,
record_candidate_row_page_cache_load, record_candidate_row_page_cache_miss,
record_candidate_row_page_cache_none, CandidatePageCache, CandidatePageCacheKey,
CandidatePageSnapshot, CandidateResolvedPageCache, CandidateResolvedPageCacheKey,
CandidateResolvedPageSnapshot, CandidateRowPageCache, CandidateRowPageCacheKey,
};
pub(crate) use dashboard_response::DashboardResponseCache;
pub(crate) use direct_plan_bypass::DirectPlanBypassCache;
+181 -4
View File
@@ -116,7 +116,7 @@ async fn balance_capacity_rejection(
return Ok(None);
}
let quota = state
.find_user_daily_quota_availability(&auth_context.user_id)
.find_user_daily_quota_availability_for_auth(&auth_context.user_id)
.await?
.filter(|quota| quota.has_active_daily_quota);
let wallet = state
@@ -201,9 +201,48 @@ async fn estimate_request_cost_upper_bound_usd(
return Ok(None);
};
let max_output_tokens = body_json.as_ref().and_then(max_output_tokens_from_request);
let cache_key = auth_request_cost_upper_bound_cache_key(
&api_format,
requested_model,
input_tokens,
max_output_tokens,
);
let ttl = state.frontdoor_runtime_guards.auth_capacity_cache_ttl;
if ttl.is_zero() {
return estimate_request_cost_upper_bound_for_tokens(
state,
&api_format,
requested_model,
input_tokens,
max_output_tokens,
)
.await;
}
state
.auth_request_cost_upper_bound_cache
.get_or_load(cache_key, ttl, || async {
estimate_request_cost_upper_bound_for_tokens(
state,
&api_format,
requested_model,
input_tokens,
max_output_tokens,
)
.await
})
.await
}
async fn estimate_request_cost_upper_bound_for_tokens(
state: &AppState,
api_format: &str,
requested_model: &str,
input_tokens: u64,
max_output_tokens: Option<u64>,
) -> Result<Option<f64>, GatewayError> {
let candidates = state
.list_minimal_candidate_selection_rows_for_api_format_and_requested_model(
&api_format,
api_format,
requested_model,
)
.await?;
@@ -223,7 +262,7 @@ async fn estimate_request_cost_upper_bound_usd(
};
let Some(estimate) = estimate_cost_from_billing_context(
&context,
&api_format,
api_format,
input_tokens,
max_output_tokens,
) else {
@@ -234,6 +273,23 @@ async fn estimate_request_cost_upper_bound_usd(
Ok(max_estimate.filter(|value| value.is_finite() && *value >= 0.0))
}
fn auth_request_cost_upper_bound_cache_key(
api_format: &str,
requested_model: &str,
input_tokens: u64,
max_output_tokens: Option<u64>,
) -> String {
format!(
"{}\x1f{}\x1f{}\x1f{}",
api_format,
requested_model,
input_tokens,
max_output_tokens
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string())
)
}
fn estimate_cost_from_billing_context(
context: &aether_data_contracts::repository::billing::StoredBillingModelContext,
api_format: &str,
@@ -469,6 +525,7 @@ fn push_unique_api_format(api_formats: &mut Vec<String>, api_format: &str) {
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
@@ -596,7 +653,7 @@ mod tests {
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_row(),
]));
let billing_repository = Arc::new(FixedBillingReadRepository { quota, context });
let billing_repository = Arc::new(FixedBillingReadRepository::new(quota, context));
let data = GatewayDataState::with_minimal_candidate_selection_and_billing_for_tests(
candidate_repository,
billing_repository,
@@ -684,6 +741,36 @@ mod tests {
struct FixedBillingReadRepository {
quota: UserDailyQuotaAvailabilityRecord,
context: StoredBillingModelContext,
quota_calls: Arc<AtomicUsize>,
model_context_by_model_id_calls: Arc<AtomicUsize>,
}
impl FixedBillingReadRepository {
fn new(
quota: UserDailyQuotaAvailabilityRecord,
context: StoredBillingModelContext,
) -> Self {
Self {
quota,
context,
quota_calls: Arc::new(AtomicUsize::new(0)),
model_context_by_model_id_calls: Arc::new(AtomicUsize::new(0)),
}
}
fn with_counters(
quota: UserDailyQuotaAvailabilityRecord,
context: StoredBillingModelContext,
quota_calls: Arc<AtomicUsize>,
model_context_by_model_id_calls: Arc<AtomicUsize>,
) -> Self {
Self {
quota,
context,
quota_calls,
model_context_by_model_id_calls,
}
}
}
#[async_trait]
@@ -703,6 +790,8 @@ mod tests {
_provider_api_key_id: Option<&str>,
_model_id: &str,
) -> Result<Option<StoredBillingModelContext>, DataLayerError> {
self.model_context_by_model_id_calls
.fetch_add(1, Ordering::AcqRel);
Ok(Some(self.context.clone()))
}
@@ -710,6 +799,7 @@ mod tests {
&self,
_user_id: &str,
) -> Result<Option<UserDailyQuotaAvailabilityRecord>, DataLayerError> {
self.quota_calls.fetch_add(1, Ordering::AcqRel);
Ok(Some(self.quota.clone()))
}
}
@@ -826,6 +916,93 @@ mod tests {
}
}
#[tokio::test]
async fn auth_capacity_reuses_quota_wallet_and_cost_estimate_within_ttl() {
let context = billing_context_with_pricing(
Some(json!({
"tiers": [{
"up_to": null,
"input_price_per_1m": 0.0,
"output_price_per_1m": 60.0
}]
})),
None,
None,
None,
);
let quota_calls = Arc::new(AtomicUsize::new(0));
let model_context_calls = Arc::new(AtomicUsize::new(0));
let candidate_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_row(),
]));
let billing_repository = Arc::new(FixedBillingReadRepository::with_counters(
quota_availability(1.0, true),
context,
Arc::clone(&quota_calls),
Arc::clone(&model_context_calls),
));
let data = GatewayDataState::with_minimal_candidate_selection_and_billing_for_tests(
candidate_repository,
billing_repository,
);
let state = AppState::new()
.expect("state should build")
.with_data_state_for_tests(data)
.with_auth_wallets_for_tests(vec![sample_wallet("user-1", 30.0)]);
let decision = decision_with_allowed_models(vec!["gpt-5".to_string()]);
let uri: Uri = "/v1/chat/completions".parse().expect("uri should parse");
let body = Bytes::from_static(
br#"{"model":"gpt-5","messages":[{"role":"user","content":"hi"}],"max_tokens":100000}"#,
);
let first =
request_model_local_rejection(&state, Some(&decision), &uri, &json_headers(), &body)
.await
.expect("first auth capacity check should resolve");
assert_eq!(first, None);
assert_eq!(quota_calls.load(Ordering::Acquire), 1);
assert_eq!(model_context_calls.load(Ordering::Acquire), 1);
let store = state
.auth_wallet_store
.as_ref()
.expect("test wallet store should exist");
{
let mut wallets = store.lock().expect("wallet store should lock");
let wallet = wallets
.get_mut("wallet-user-1")
.expect("test wallet should exist");
wallet.balance = 0.0;
wallet.gift_balance = 0.0;
}
let cached =
request_model_local_rejection(&state, Some(&decision), &uri, &json_headers(), &body)
.await
.expect("cached auth capacity check should resolve");
assert_eq!(cached, None);
assert_eq!(quota_calls.load(Ordering::Acquire), 1);
assert_eq!(model_context_calls.load(Ordering::Acquire), 1);
state.invalidate_auth_context_cache();
let refreshed =
request_model_local_rejection(&state, Some(&decision), &uri, &json_headers(), &body)
.await
.expect("refreshed auth capacity check should resolve");
assert_eq!(
refreshed,
Some(GatewayLocalAuthRejection::BalanceDenied {
remaining: Some(1.0),
})
);
assert_eq!(quota_calls.load(Ordering::Acquire), 2);
}
#[tokio::test]
async fn admin_bypass_limits_does_not_skip_exhausted_daily_quota_capacity() {
let context = billing_context_with_pricing(
@@ -10,7 +10,9 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::{debug, info};
use crate::wallet_runtime::{local_rejection_from_wallet_access, resolve_wallet_auth_gate};
use crate::wallet_runtime::{
local_rejection_from_wallet_access, resolve_wallet_auth_gate, resolve_wallet_auth_gate_uncached,
};
use crate::{AppState, GatewayError};
use super::super::GatewayControlDecision;
@@ -700,7 +702,7 @@ pub(crate) async fn refresh_execution_runtime_auth_context(
return Ok(denied);
};
let wallet_access = resolve_wallet_auth_gate(state, &snapshot).await?;
let wallet_access = resolve_wallet_auth_gate_uncached(state, &snapshot).await?;
Ok(build_data_backed_auth_context(
state,
snapshot,
@@ -368,6 +368,12 @@ impl GatewayDataState {
}
}
pub(crate) fn clear_routing_group_cache(&self) {
if let Some(repository) = &self.routing_group_reader {
repository.clear_local_cache();
}
}
pub(crate) fn clear_provider_catalog_cache(&self) {
if let Some(repository) = &self.provider_catalog_reader {
repository.clear_local_cache();
@@ -360,3 +360,5 @@ mod routing_profiles;
mod runtime;
#[cfg(test)]
mod testing;
#[cfg(feature = "testkit")]
pub(crate) mod testkit;
@@ -9,14 +9,16 @@ use aether_data_contracts::repository::routing_profiles::{
StoredRoutingGroupVersion,
};
use async_trait::async_trait;
use dashmap::DashMap;
const ROUTING_GROUP_CACHE_TTL: Duration = Duration::from_secs(5);
const ROUTING_GROUP_CACHE_STALE_TTL: Duration = Duration::from_secs(60);
const ROUTING_GROUP_CACHE_MAX_ENTRIES: usize = 4_096;
const ROUTING_GROUP_CACHE_MAX_LOAD_GUARDS: usize = 4_096;
pub(super) struct CachedRoutingGroupReadRepository {
inner: Arc<dyn RoutingGroupReadRepository>,
entries: ExpiringMap<RoutingGroupCacheKey, RoutingGroupCacheValue>,
load_guard: tokio::sync::Mutex<()>,
load_guards: DashMap<RoutingGroupCacheKey, Arc<tokio::sync::Mutex<()>>>,
}
impl CachedRoutingGroupReadRepository {
@@ -24,31 +26,53 @@ impl CachedRoutingGroupReadRepository {
Self {
inner,
entries: ExpiringMap::new(),
load_guard: tokio::sync::Mutex::new(()),
load_guards: DashMap::new(),
}
}
fn clear(&self) {
self.entries.clear();
self.load_guards.clear();
}
async fn get_or_load(
&self,
key: RoutingGroupCacheKey,
load: impl std::future::Future<Output = Result<RoutingGroupCacheValue, DataLayerError>>,
) -> Result<RoutingGroupCacheValue, DataLayerError> {
if let Some(value) = self.entries.get_fresh(&key, ROUTING_GROUP_CACHE_TTL) {
if let Some((value, _age)) = self
.entries
.get_with_age(&key, ROUTING_GROUP_CACHE_STALE_TTL)
{
return Ok(value);
}
let _guard = self.load_guard.lock().await;
if let Some(value) = self.entries.get_fresh(&key, ROUTING_GROUP_CACHE_TTL) {
let load_guard = self.load_guard_for(&key);
let _guard = load_guard.lock().await;
if let Some((value, _age)) = self
.entries
.get_with_age(&key, ROUTING_GROUP_CACHE_STALE_TTL)
{
return Ok(value);
}
let value = load.await?;
self.entries.insert(
key,
value.clone(),
ROUTING_GROUP_CACHE_TTL,
ROUTING_GROUP_CACHE_STALE_TTL,
ROUTING_GROUP_CACHE_MAX_ENTRIES,
);
Ok(value)
}
fn load_guard_for(&self, key: &RoutingGroupCacheKey) -> Arc<tokio::sync::Mutex<()>> {
if self.load_guards.len() > ROUTING_GROUP_CACHE_MAX_LOAD_GUARDS {
self.load_guards.clear();
}
self.load_guards
.entry(key.clone())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -92,6 +116,10 @@ fn subject_cache_key(subject: Option<RoutingGroupBindingSubject>) -> Option<&'st
#[async_trait]
impl RoutingGroupReadRepository for CachedRoutingGroupReadRepository {
fn clear_local_cache(&self) {
self.clear();
}
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
match self
.get_or_load(RoutingGroupCacheKey::ListGroups, async {
@@ -168,3 +196,67 @@ impl RoutingGroupReadRepository for CachedRoutingGroupReadRepository {
}
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
#[derive(Default)]
struct CountingRoutingGroupReadRepository {
list_calls: AtomicUsize,
}
#[async_trait]
impl RoutingGroupReadRepository for CountingRoutingGroupReadRepository {
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, DataLayerError> {
self.list_calls.fetch_add(1, Ordering::AcqRel);
Ok(Vec::new())
}
async fn find_routing_group(
&self,
_lookup: RoutingGroupLookupKey<'_>,
) -> Result<Option<StoredRoutingGroup>, DataLayerError> {
Ok(None)
}
async fn list_routing_group_bindings(
&self,
_query: &RoutingGroupBindingQuery,
) -> Result<Vec<StoredRoutingGroupBinding>, DataLayerError> {
Ok(Vec::new())
}
async fn list_routing_group_versions(
&self,
_group_id: &str,
) -> Result<Vec<StoredRoutingGroupVersion>, DataLayerError> {
Ok(Vec::new())
}
}
#[tokio::test]
async fn clear_local_cache_forces_next_load() {
let inner = Arc::new(CountingRoutingGroupReadRepository::default());
let repository = CachedRoutingGroupReadRepository::new(inner.clone());
repository
.list_routing_groups()
.await
.expect("initial list should load");
repository
.list_routing_groups()
.await
.expect("cached list should load");
assert_eq!(inner.list_calls.load(Ordering::Acquire), 1);
repository.clear_local_cache();
repository
.list_routing_groups()
.await
.expect("cleared list should reload");
assert_eq!(inner.list_calls.load(Ordering::Acquire), 2);
}
}
@@ -0,0 +1,91 @@
use std::sync::Arc;
use aether_data_contracts::repository::candidate_selection::MinimalCandidateSelectionReadRepository;
use aether_data_contracts::repository::candidates::{
RequestCandidateReadRepository, RequestCandidateRepository, RequestCandidateWriteRepository,
};
use aether_data_contracts::repository::provider_catalog::{
ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
};
use aether_data_contracts::repository::usage::{
UsageReadRepository, UsageRepository, UsageWriteRepository,
};
use aether_data::repository::auth::AuthApiKeyReadRepository;
use super::{GatewayDataConfig, GatewayDataState};
impl GatewayDataState {
pub(crate) fn with_openai_chat_pressure_repositories_for_testkit<T, U, V>(
auth_api_key_repository: Arc<dyn AuthApiKeyReadRepository>,
candidate_selection_repository: Arc<dyn MinimalCandidateSelectionReadRepository>,
provider_catalog_repository: Arc<U>,
request_candidate_repository: Arc<T>,
usage_repository: Arc<V>,
encryption_key: impl Into<String>,
) -> Self
where
T: RequestCandidateRepository + 'static,
U: ProviderCatalogReadRepository + ProviderCatalogWriteRepository + 'static,
V: UsageRepository + 'static,
{
let request_candidate_reader: Arc<dyn RequestCandidateReadRepository> =
request_candidate_repository.clone();
let request_candidate_writer: Arc<dyn RequestCandidateWriteRepository> =
request_candidate_repository;
let provider_catalog_reader: Arc<dyn ProviderCatalogReadRepository> =
provider_catalog_repository.clone();
let provider_catalog_writer: Arc<dyn ProviderCatalogWriteRepository> =
provider_catalog_repository;
let usage_reader: Arc<dyn UsageReadRepository> = usage_repository.clone();
let usage_writer: Arc<dyn UsageWriteRepository> = usage_repository;
Self {
config: GatewayDataConfig::disabled().with_encryption_key(encryption_key),
backends: None,
auth_api_key_reader: Some(auth_api_key_repository),
auth_api_key_writer: None,
auth_module_reader: None,
auth_module_writer: None,
announcement_reader: None,
announcement_writer: None,
management_token_reader: None,
management_token_writer: None,
oauth_provider_reader: None,
oauth_provider_writer: None,
proxy_node_reader: None,
proxy_node_writer: None,
billing_reader: None,
background_task_reader: None,
background_task_writer: None,
gemini_file_mapping_reader: None,
gemini_file_mapping_writer: None,
global_model_reader: None,
global_model_writer: None,
minimal_candidate_selection_reader: Some(candidate_selection_repository),
request_candidate_reader: Some(request_candidate_reader),
request_candidate_writer: Some(request_candidate_writer),
provider_catalog_reader: Some(provider_catalog_reader),
provider_catalog_writer: Some(provider_catalog_writer),
pool_score_reader: None,
pool_score_writer: None,
provider_quota_reader: None,
provider_quota_writer: None,
routing_group_reader: None,
routing_group_writer: None,
usage_reader: Some(usage_reader),
usage_writer: Some(usage_writer),
user_reader: None,
user_preferences: None,
usage_worker_queue: None,
video_task_reader: None,
video_task_writer: None,
wallet_reader: None,
wallet_writer: None,
settlement_writer: None,
system_config_values: None,
system_config_value_cache: Default::default(),
billing_model_context_cache: Default::default(),
}
}
}
@@ -15,6 +15,7 @@ use base64::Engine as _;
use futures_util::stream::{self, BoxStream};
use futures_util::StreamExt;
use http::{HeaderMap, HeaderName, HeaderValue};
use http_body_util::BodyExt;
use regex::{Captures, Regex};
use serde_json::{json, Map, Value};
use uuid::Uuid;
@@ -29,10 +30,10 @@ use crate::clock::current_unix_secs;
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
use crate::execution_runtime::transport::{
build_browser_wreq_client, build_request_body, build_request_headers,
decode_response_body_bytes, format_upstream_request_error, format_wreq_upstream_request_error,
resolve_stream_first_byte_timeout, send_request, stream_first_byte_timeout_message,
with_non_stream_total_timeout, DirectHttpResponse, ExecutionRuntimeTransportError,
ExecutionTransportControls,
decode_response_body_bytes, format_hyper_error_chain, format_upstream_request_error,
format_wreq_upstream_request_error, resolve_stream_first_byte_timeout, send_request,
stream_first_byte_timeout_message, with_non_stream_total_timeout, DirectHttpResponse,
ExecutionRuntimeTransportError, ExecutionTransportControls,
};
const GROK_INTERNAL_HEADER: &str = "x-aether-grok-runtime";
@@ -503,6 +504,15 @@ async fn collect_grok_response_stream(
collect_grok_response_chunk(status_code, upstream_bytes, raw_body, adapter, &chunk);
}
}
DirectHttpResponse::HyperH2c(response) => {
let mut stream = response.into_body().into_data_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|err| {
ExecutionRuntimeTransportError::UpstreamRequest(format_hyper_error_chain(&err))
})?;
collect_grok_response_chunk(status_code, upstream_bytes, raw_body, adapter, &chunk);
}
}
DirectHttpResponse::BrowserWreq(response) => {
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
@@ -547,6 +557,16 @@ fn grok_response_body_stream(response: DirectHttpResponse) -> GrokUpstreamBodySt
})
})
.boxed(),
DirectHttpResponse::HyperH2c(response) => response
.into_body()
.into_data_stream()
.map(|chunk| {
chunk.map_err(|err| {
ExecutionRuntimeTransportError::UpstreamRequest(format_hyper_error_chain(&err))
.to_string()
})
})
.boxed(),
DirectHttpResponse::BrowserWreq(response) => response
.bytes_stream()
.map(|chunk| {
@@ -48,6 +48,15 @@ pub use server::{
build_execution_runtime_router_with_request_gates, serve_execution_runtime_tcp,
serve_execution_runtime_unix,
};
pub use transport::DirectH2cSenderPrewarmReport;
pub async fn prewarm_direct_h2c_sender_cache_from_env_for_startup(
) -> Result<Option<DirectH2cSenderPrewarmReport>, String> {
transport::prewarm_direct_h2c_sender_cache_from_env()
.await
.map_err(|err| err.to_string())
}
pub(crate) use stream::execute_execution_runtime_stream;
pub(crate) use stream_pump::build_direct_execution_frame_stream;
pub(crate) use sync::{
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ use async_stream::stream;
use axum::body::Bytes;
use base64::Engine as _;
use futures_util::{Stream, StreamExt};
use http_body_util::BodyExt;
use serde_json::Value;
use tracing::warn;
@@ -20,7 +21,8 @@ use crate::ai_serving::api::{
};
use crate::execution_runtime::ndjson::encode_stream_frame_ndjson;
use crate::execution_runtime::transport::{
format_wreq_upstream_request_error, stream_first_byte_timeout_message, DirectUpstreamResponse,
format_hyper_error_chain, format_wreq_upstream_request_error,
stream_first_byte_timeout_message, DirectUpstreamResponse,
};
use crate::execution_runtime::DirectUpstreamStreamExecution;
use crate::GatewayError;
@@ -273,6 +275,88 @@ pub(crate) fn build_direct_execution_frame_stream(
}
}
}
DirectUpstreamResponse::HyperH2c(response) => {
let mut bytes_stream = response.into_body().into_data_stream();
loop {
let item = if ttfb_ms.is_none() {
match await_stream_first_byte(
bytes_stream.next(),
started_at,
stream_first_byte_timeout,
)
.await
{
Ok(item) => item,
Err(timeout) => {
match encode_first_byte_timeout_frame(timeout) {
Ok(frame) => yield Ok(frame),
Err(err) => {
yield Err(err);
return;
}
}
break;
}
}
} else {
bytes_stream.next().await
};
let Some(item) = item else {
break;
};
match item {
Ok(chunk) => {
if ttfb_ms.is_none() {
ttfb_ms = Some(started_at.elapsed().as_millis() as u64);
}
if !first_chunk_telemetry_emitted {
match encode_telemetry_frame(ttfb_ms, ttfb_ms, upstream_bytes) {
Ok(frame) => yield Ok(frame),
Err(err) => {
yield Err(err);
return;
}
}
first_chunk_telemetry_emitted = true;
}
upstream_bytes += chunk.len() as u64;
observe_stream_chunk(
&mut stream_terminal_observer,
&normalized_observer_context,
private_stream_normalizer.as_mut(),
&mut observer_buffered,
chunk.as_ref(),
);
match encode_data_frame(&chunk) {
Ok(frame) => yield Ok(frame),
Err(err) => {
yield Err(err);
return;
}
}
}
Err(err) => {
let message = format_hyper_error_chain(&err);
warn!(
event_name = "stream_pump_body_read_error",
log_type = "ops",
status_code,
upstream_bytes,
error = %message,
"upstream body stream read error"
);
match encode_error_frame(status_code, message) {
Ok(frame) => yield Ok(frame),
Err(encode_err) => {
yield Err(encode_err);
return;
}
}
break;
}
}
}
}
DirectUpstreamResponse::BrowserWreq(response) => {
let mut bytes_stream = response.bytes_stream();
loop {
@@ -662,6 +746,60 @@ async fn buffer_non_sse_upstream_body(
}
}
}
DirectUpstreamResponse::HyperH2c(response) => {
let mut bytes_stream = response.into_body().into_data_stream();
loop {
let item = if ttfb_ms.is_none() {
match await_stream_first_byte(
bytes_stream.next(),
started_at,
stream_first_byte_timeout,
)
.await
{
Ok(item) => item,
Err(timeout) => {
return Err(BufferedUpstreamBodyError {
message: stream_first_byte_timeout_message(timeout),
ttfb_ms,
upstream_bytes,
first_byte_timeout: Some(timeout),
});
}
}
} else {
bytes_stream.next().await
};
let Some(item) = item else {
break;
};
match item {
Ok(chunk) => {
if ttfb_ms.is_none() {
ttfb_ms = Some(started_at.elapsed().as_millis() as u64);
}
upstream_bytes += chunk.len() as u64;
body_bytes.extend_from_slice(&chunk);
}
Err(err) => {
let message = format_hyper_error_chain(&err);
warn!(
event_name = "stream_pump_body_read_error",
log_type = "ops",
upstream_bytes,
error = %message,
"upstream body stream read error"
);
return Err(BufferedUpstreamBodyError {
message,
ttfb_ms,
upstream_bytes,
first_byte_timeout: None,
});
}
}
}
}
DirectUpstreamResponse::BrowserWreq(response) => {
let mut bytes_stream = response.bytes_stream();
loop {
@@ -22,6 +22,7 @@ use axum::body::{to_bytes, Body, Bytes};
use axum::http::header::{CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE};
use axum::http::{HeaderName, HeaderValue, Response, StatusCode};
use futures_util::StreamExt;
use http_body_util::BodyExt;
use serde_json::{json, Value};
use tokio::sync::mpsc;
use tokio::sync::Mutex;
@@ -55,9 +56,9 @@ use crate::execution_runtime::submission::{
};
use crate::execution_runtime::transport::{
build_execution_response_body, build_request_body, collect_response_headers,
decode_response_body_bytes, format_upstream_request_error, format_wreq_upstream_request_error,
response_body_is_json, send_request, DirectHttpResponse, DirectSyncExecutionRuntime,
ExecutionRuntimeTransportError,
decode_response_body_bytes, format_hyper_error_chain, format_upstream_request_error,
format_wreq_upstream_request_error, response_body_is_json, send_request, DirectHttpResponse,
DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
};
use crate::execution_runtime::windsurf::maybe_execute_windsurf_sync;
use crate::execution_runtime::{
@@ -1165,6 +1166,23 @@ async fn execute_openai_image_sync_upstream_sse_candidate(
body_bytes.extend_from_slice(&chunk);
}
}
DirectHttpResponse::HyperH2c(response) => {
let mut upstream_stream = response.into_body().into_data_stream();
while let Some(chunk) = upstream_stream.next().await {
let chunk = chunk.map_err(|err| {
SyncExecutionFailure::from_transport(
ExecutionRuntimeTransportError::UpstreamRequest(format_hyper_error_chain(
&err,
)),
)
})?;
let elapsed_ms = started_at.elapsed().as_millis() as u64;
progress
.observe_chunk(&chunk, status_code, elapsed_ms)
.await;
body_bytes.extend_from_slice(&chunk);
}
}
DirectHttpResponse::BrowserWreq(response) => {
let mut upstream_stream = response.bytes_stream();
while let Some(chunk) = upstream_stream.next().await {
@@ -1522,7 +1540,8 @@ async fn execute_execution_runtime_sync_impl(
let lifecycle_seed = build_lifecycle_usage_seed(&plan, report_context.as_ref());
state
.usage_runtime
.record_pending(state.data.as_ref(), lifecycle_seed);
.record_pending_direct(state.data.as_ref(), lifecycle_seed)
.await;
record_local_request_candidate_status(
state,
&plan,
File diff suppressed because it is too large Load Diff
@@ -29,6 +29,12 @@ use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AppState, GatewayError};
const DEFAULT_STREAM_FIRST_BYTE_WATCHDOG_TIMEOUT_MS: u64 = 30_000;
const UPSTREAM_EXECUTION_GATE_NAME: &str = "gateway_upstream_execution";
const UPSTREAM_TARGET_GATE_NAME: &str = "gateway_upstream_target";
const UPSTREAM_EXECUTION_GATE_HOLD_STREAM_RESPONSE_ENV: &str =
"AETHER_GATEWAY_UPSTREAM_EXECUTION_GATE_HOLD_STREAM_RESPONSE";
const UPSTREAM_EXECUTION_GATE_STREAM_HOLD_MODE_ENV: &str =
"AETHER_GATEWAY_UPSTREAM_EXECUTION_GATE_STREAM_HOLD_MODE";
fn attach_redaction_execution_candidate(response: &mut Response<Body>, candidate_id: Option<&str>) {
if let Some(candidate_id) = candidate_id
@@ -162,6 +168,7 @@ where
async fn execute_attempt(&self, attempt: &T) -> Result<Option<Self::Response>, Self::Error> {
prewarm_direct_reqwest_candidate_client(attempt.execution_plan());
let _permit = acquire_upstream_execution_gate(self.state, self.trace_id).await?;
let upstream_execution_gate_held_started_at = std::time::Instant::now();
let mut response = execute_execution_runtime_sync(
self.state,
self.parts.uri.path(),
@@ -173,6 +180,12 @@ where
attempt.report_context(),
)
.await?;
observe_gateway_stage_ms(
"upstream_execution_gate_held",
upstream_execution_gate_held_started_at
.elapsed()
.as_millis() as u64,
);
if let Some(response) = response.as_mut() {
attach_redaction_execution_candidate(
response,
@@ -577,6 +590,104 @@ fn stream_candidate_watchdog_timeout_message() -> &'static str {
"Stream first byte timeout"
}
fn admission_timeout_gate(error: &GatewayError) -> Option<&'static str> {
match error {
GatewayError::AdmissionTimeout { gate, .. } => Some(*gate),
_ => None,
}
}
fn admission_timeout_message(error: &GatewayError) -> String {
match error {
GatewayError::AdmissionTimeout {
gate,
queue_budget_ms,
..
} => {
format!("gateway admission gate {gate} timed out after {queue_budget_ms}ms")
}
other => format!("{other:?}"),
}
}
fn is_candidate_level_admission_timeout(error: &GatewayError) -> bool {
matches!(
admission_timeout_gate(error),
Some(UPSTREAM_EXECUTION_GATE_NAME | UPSTREAM_TARGET_GATE_NAME)
)
}
fn should_record_candidate_admission_timeout(error: &GatewayError) -> bool {
matches!(
admission_timeout_gate(error),
Some(UPSTREAM_EXECUTION_GATE_NAME)
)
}
async fn record_stream_candidate_admission_timeout(
state: &(impl RequestCandidateRuntimeWriter + ?Sized),
plan: &aether_contracts::ExecutionPlan,
report_context: Option<&serde_json::Value>,
candidate_started_unix_ms: u64,
error: &GatewayError,
) {
let terminal_unix_ms = current_unix_ms();
record_local_request_candidate_status(
state,
plan,
report_context,
SchedulerRequestCandidateStatusUpdate {
status: RequestCandidateStatus::Failed,
status_code: Some(http::StatusCode::TOO_MANY_REQUESTS.as_u16()),
error_type: Some("gateway_admission_timeout".to_string()),
error_message: Some(admission_timeout_message(error)),
latency_ms: Some(terminal_unix_ms.saturating_sub(candidate_started_unix_ms)),
started_at_unix_ms: Some(candidate_started_unix_ms),
finished_at_unix_ms: Some(terminal_unix_ms),
},
)
.await;
}
fn log_stream_candidate_admission_timeout(
trace_id: &str,
plan_kind: &str,
plan: &aether_contracts::ExecutionPlan,
report_context: Option<&serde_json::Value>,
error: &GatewayError,
) {
let provider_name = plan.provider_name.as_deref().unwrap_or("-");
let model_name = plan.model_name.as_deref().unwrap_or("-");
let candidate_index = parse_request_candidate_report_context(report_context)
.and_then(|context| context.candidate_index)
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string());
let (gate, queue_budget_ms) = match error {
GatewayError::AdmissionTimeout {
gate,
queue_budget_ms,
..
} => (*gate, *queue_budget_ms),
_ => ("-", 0),
};
warn!(
event_name = "local_stream_candidate_admission_timeout",
log_type = "event",
trace_id = %trace_id,
plan_kind,
request_id = %short_request_id(plan.request_id.as_str()),
candidate_id = ?plan.candidate_id,
provider_name,
endpoint_id = %plan.endpoint_id,
key_id = %plan.key_id,
model_name,
candidate_index = candidate_index.as_str(),
gate,
queue_budget_ms,
"gateway local stream candidate admission timed out; retrying next candidate"
);
}
async fn execute_stream_candidate_with_watchdog<Fut>(
state: &(impl RequestCandidateRuntimeWriter + UpstreamExecutionGateProvider + ?Sized),
trace_id: &str,
@@ -586,22 +697,31 @@ async fn execute_stream_candidate_with_watchdog<Fut>(
execute: impl FnOnce() -> Fut,
) -> Result<Option<Response<Body>>, GatewayError>
where
Fut:
std::future::Future<Output = Result<Option<Response<Body>>, GatewayError>> + Send + 'static,
Fut: std::future::Future<Output = Result<Option<Response<Body>>, GatewayError>> + Send,
{
let timeout_duration = resolve_stream_candidate_watchdog_timeout(plan, report_context);
let candidate_started_unix_ms = current_unix_ms();
let permit = acquire_upstream_execution_gate(state, trace_id).await?;
let mut join_handle = tokio::spawn(execute());
match timeout(timeout_duration, &mut join_handle).await {
Ok(Ok(result)) => {
result.map(|response| maybe_hold_upstream_execution_permit(response, permit))
let permit = match acquire_upstream_execution_gate(state, trace_id).await {
Ok(permit) => permit,
Err(err) if is_candidate_level_admission_timeout(&err) => {
record_stream_candidate_admission_timeout(
state,
plan,
report_context,
candidate_started_unix_ms,
&err,
)
.await;
log_stream_candidate_admission_timeout(trace_id, plan_kind, plan, report_context, &err);
return Ok(None);
}
Ok(Err(join_error)) => Err(GatewayError::Internal(format!(
"local stream candidate task join failed: {join_error}"
))),
Err(err) => return Err(err),
};
let permit_hold = permit.map(UpstreamExecutionPermitHold::new);
let watchdog_started_at = std::time::Instant::now();
let outcome = match timeout(timeout_duration, execute()).await {
Ok(result) => result,
Err(_) => {
join_handle.abort();
let finished_at_unix_ms = current_unix_ms();
let request_id = short_request_id(plan.request_id.as_str());
let provider_name = plan.provider_name.as_deref().unwrap_or("-");
@@ -643,28 +763,144 @@ where
);
Ok(None)
}
};
observe_gateway_stage_ms(
"stream_candidate_watchdog_inline",
watchdog_started_at.elapsed().as_millis() as u64,
);
match outcome {
Ok(response) => Ok(maybe_hold_upstream_execution_permit(response, permit_hold)),
Err(err) if is_candidate_level_admission_timeout(&err) => {
drop(permit_hold);
if should_record_candidate_admission_timeout(&err) {
record_stream_candidate_admission_timeout(
state,
plan,
report_context,
candidate_started_unix_ms,
&err,
)
.await;
}
log_stream_candidate_admission_timeout(trace_id, plan_kind, plan, report_context, &err);
Ok(None)
}
Err(err) => {
drop(permit_hold);
Err(err)
}
}
}
struct UpstreamExecutionPermitHold {
_permit: ConcurrencyPermit,
started_at: std::time::Instant,
}
impl UpstreamExecutionPermitHold {
fn new(permit: ConcurrencyPermit) -> Self {
Self {
_permit: permit,
started_at: std::time::Instant::now(),
}
}
}
impl Drop for UpstreamExecutionPermitHold {
fn drop(&mut self) {
observe_gateway_stage_ms(
"upstream_execution_gate_held",
self.started_at.elapsed().as_millis() as u64,
);
}
}
fn maybe_hold_upstream_execution_permit(
response: Option<Response<Body>>,
permit: Option<ConcurrencyPermit>,
permit_hold: Option<UpstreamExecutionPermitHold>,
) -> Option<Response<Body>> {
match (response, permit) {
(Some(response), Some(permit)) => {
Some(hold_response_upstream_execution_permit(response, permit))
match upstream_execution_gate_stream_hold_mode() {
UpstreamExecutionStreamHoldMode::Headers => {
drop(permit_hold);
response
}
(response, _) => response,
UpstreamExecutionStreamHoldMode::FirstBody => match (response, permit_hold) {
(Some(response), Some(permit_hold)) => Some(
hold_response_upstream_execution_permit_until_first_body(response, permit_hold),
),
(response, _permit_hold) => response,
},
UpstreamExecutionStreamHoldMode::Response => match (response, permit_hold) {
(Some(response), Some(permit_hold)) => Some(hold_response_upstream_execution_permit(
response,
permit_hold,
)),
(response, _permit_hold) => response,
},
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum UpstreamExecutionStreamHoldMode {
Headers,
FirstBody,
Response,
}
fn upstream_execution_gate_stream_hold_mode() -> UpstreamExecutionStreamHoldMode {
if std::env::var(UPSTREAM_EXECUTION_GATE_HOLD_STREAM_RESPONSE_ENV)
.ok()
.is_some_and(|value| parse_env_bool(value.as_str()))
{
return UpstreamExecutionStreamHoldMode::Response;
}
std::env::var(UPSTREAM_EXECUTION_GATE_STREAM_HOLD_MODE_ENV)
.ok()
.as_deref()
.map(parse_upstream_execution_stream_hold_mode)
.unwrap_or(UpstreamExecutionStreamHoldMode::FirstBody)
}
fn parse_upstream_execution_stream_hold_mode(value: &str) -> UpstreamExecutionStreamHoldMode {
match value.trim().to_ascii_lowercase().as_str() {
"headers" | "header" | "off" | "none" | "disabled" | "disable" | "0" => {
UpstreamExecutionStreamHoldMode::Headers
}
"response" | "full" | "body" | "stream" | "1" => UpstreamExecutionStreamHoldMode::Response,
_ => UpstreamExecutionStreamHoldMode::FirstBody,
}
}
fn parse_env_bool(value: &str) -> bool {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
}
fn hold_response_upstream_execution_permit_until_first_body(
response: Response<Body>,
permit_hold: UpstreamExecutionPermitHold,
) -> Response<Body> {
let (parts, body) = response.into_parts();
let stream = async_stream::stream! {
let mut permit_hold = Some(permit_hold);
let mut body_stream = body.into_data_stream();
while let Some(item) = body_stream.next().await {
drop(permit_hold.take());
yield item;
}
};
Response::from_parts(parts, Body::from_stream(stream))
}
fn hold_response_upstream_execution_permit(
response: Response<Body>,
permit: ConcurrencyPermit,
permit_hold: UpstreamExecutionPermitHold,
) -> Response<Body> {
let (parts, body) = response.into_parts();
let stream = async_stream::stream! {
let _permit = permit;
let _permit_hold = permit_hold;
let mut body_stream = body.into_data_stream();
while let Some(item) = body_stream.next().await {
yield item;
@@ -696,12 +932,19 @@ async fn acquire_upstream_execution_gate(
return Ok(None);
};
let budget = state.upstream_execution_gate_queue_budget();
let gate_wait_started_at = std::time::Instant::now();
match timeout(budget, gate.acquire()).await {
Ok(Ok(permit)) => Ok(Some(permit)),
Ok(Ok(permit)) => {
observe_gateway_stage_ms(
"upstream_execution_gate_wait",
gate_wait_started_at.elapsed().as_millis() as u64,
);
Ok(Some(permit))
}
Ok(Err(err)) => Err(GatewayError::Internal(err.to_string())),
Err(_) => Err(GatewayError::AdmissionTimeout {
trace_id: trace_id.to_string(),
gate: "gateway_upstream_execution",
gate: UPSTREAM_EXECUTION_GATE_NAME,
queue_budget_ms: budget.as_millis() as u64,
}),
}
@@ -753,9 +996,33 @@ mod tests {
use super::*;
#[derive(Debug, Default)]
struct TestRequestCandidateWriter {
records: Mutex<Vec<UpsertRequestCandidateRecord>>,
upstream_gate: Option<aether_runtime::ConcurrencyGate>,
upstream_queue_budget: Duration,
}
impl Default for TestRequestCandidateWriter {
fn default() -> Self {
Self {
records: Mutex::new(Vec::new()),
upstream_gate: None,
upstream_queue_budget: Duration::from_millis(250),
}
}
}
impl TestRequestCandidateWriter {
fn with_upstream_gate(limit: usize, queue_budget: Duration) -> Self {
Self {
records: Mutex::new(Vec::new()),
upstream_gate: Some(aether_runtime::ConcurrencyGate::new(
UPSTREAM_EXECUTION_GATE_NAME,
limit,
)),
upstream_queue_budget: queue_budget,
}
}
}
#[async_trait]
@@ -778,11 +1045,11 @@ mod tests {
impl UpstreamExecutionGateProvider for TestRequestCandidateWriter {
fn upstream_execution_gate(&self) -> Option<&aether_runtime::ConcurrencyGate> {
None
self.upstream_gate.as_ref()
}
fn upstream_execution_gate_queue_budget(&self) -> Duration {
Duration::from_millis(250)
self.upstream_queue_budget
}
}
@@ -951,6 +1218,26 @@ mod tests {
assert_eq!(timeout, Duration::from_millis(12_345));
}
#[test]
fn upstream_execution_stream_hold_mode_defaults_to_first_body() {
assert_eq!(
parse_upstream_execution_stream_hold_mode(""),
UpstreamExecutionStreamHoldMode::FirstBody
);
assert_eq!(
parse_upstream_execution_stream_hold_mode("first_body"),
UpstreamExecutionStreamHoldMode::FirstBody
);
assert_eq!(
parse_upstream_execution_stream_hold_mode("off"),
UpstreamExecutionStreamHoldMode::Headers
);
assert_eq!(
parse_upstream_execution_stream_hold_mode("response"),
UpstreamExecutionStreamHoldMode::Response
);
}
#[test]
fn unused_persistence_skips_pool_internal_candidates() {
assert!(should_skip_unused_persistence(Some(&json!({
@@ -1013,4 +1300,77 @@ mod tests {
.is_some_and(|message| message == "Stream first byte timeout"));
assert_eq!(record.candidate_index, 2);
}
#[tokio::test]
async fn stream_candidate_upstream_execution_admission_timeout_marks_failed_and_continues() {
let writer = Arc::new(TestRequestCandidateWriter::with_upstream_gate(
1,
Duration::from_millis(1),
));
let _held_permit = writer
.upstream_gate
.as_ref()
.expect("test gate should exist")
.try_acquire()
.expect("test gate permit should acquire");
let plan = test_plan(None);
let report_context = test_report_context();
let result = execute_stream_candidate_with_watchdog(
writer.as_ref(),
"trace_admission",
"claude_cli_stream",
&plan,
Some(&report_context),
|| async {
panic!("execute future should not run while upstream execution gate is saturated")
},
)
.await;
assert!(matches!(result, Ok(None)));
let records = writer.records.lock().await;
assert_eq!(records.len(), 1);
let record = &records[0];
assert_eq!(record.status, RequestCandidateStatus::Failed);
assert_eq!(
record.status_code,
Some(http::StatusCode::TOO_MANY_REQUESTS.as_u16())
);
assert_eq!(
record.error_type.as_deref(),
Some("gateway_admission_timeout")
);
assert!(record
.error_message
.as_deref()
.is_some_and(|message| message.contains(UPSTREAM_EXECUTION_GATE_NAME)));
assert_eq!(record.candidate_index, 2);
}
#[tokio::test]
async fn stream_candidate_target_admission_timeout_continues_without_duplicate_record() {
let writer = Arc::new(TestRequestCandidateWriter::default());
let plan = test_plan(None);
let report_context = test_report_context();
let result = execute_stream_candidate_with_watchdog(
writer.as_ref(),
"trace_target_admission",
"claude_cli_stream",
&plan,
Some(&report_context),
|| async {
Err(GatewayError::AdmissionTimeout {
trace_id: "trace_target_admission".to_string(),
gate: UPSTREAM_TARGET_GATE_NAME,
queue_budget_ms: 5,
})
},
)
.await;
assert!(matches!(result, Ok(None)));
assert!(writer.records.lock().await.is_empty());
}
}
@@ -1,5 +1,7 @@
use std::collections::{BTreeMap, VecDeque};
use std::future::Future;
use std::io::Error as IoError;
use std::pin::Pin;
use std::time::Instant;
use axum::body::{to_bytes, Body, Bytes};
@@ -52,6 +54,7 @@ use crate::executor::{
LocalExecutionExhaustion, LocalExecutionRequestOutcome,
};
use crate::handlers::shared::system_config_bool;
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AiExecutionDecision, AppState, GatewayError};
const ENABLE_OPENAI_IMAGE_SYNC_HEARTBEAT_CONFIG_KEY: &str = "enable_openai_image_sync_heartbeat";
@@ -192,15 +195,20 @@ pub(crate) async fn maybe_execute_stream_via_local_decision(
body_json: &serde_json::Value,
plan_kind: &str,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let Some((attempt_source, candidate_count)) =
build_local_openai_chat_stream_attempt_source_for_kind(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
else {
let attempt_source_started_at = std::time::Instant::now();
let attempt_source = build_local_openai_chat_stream_attempt_source_for_kind(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await;
observe_gateway_stage_ms(
"stream_openai_chat_attempt_source_init",
attempt_source_started_at.elapsed().as_millis() as u64,
);
let Some((attempt_source, candidate_count)) = attempt_source? else {
return Ok(LocalExecutionRequestOutcome::NoPath);
};
let attempt_source_execute_started_at = std::time::Instant::now();
let outcome = execute_stream_attempt_source::<AiStreamAttempt, _>(
state,
trace_id,
@@ -208,7 +216,12 @@ pub(crate) async fn maybe_execute_stream_via_local_decision(
plan_kind,
attempt_source,
)
.await?;
.await;
observe_gateway_stage_ms(
"stream_openai_chat_attempt_source_execute",
attempt_source_execute_started_at.elapsed().as_millis() as u64,
);
let outcome = outcome?;
if let LocalExecutionRequestOutcome::Exhausted(_) = &outcome {
set_local_openai_chat_execution_exhausted_diagnostic(
@@ -1414,66 +1427,72 @@ pub(crate) async fn maybe_execute_sync_via_local_video_decision(
.await
}
pub(crate) async fn maybe_execute_sync_request(
state: &AppState,
parts: &http::request::Parts,
body_bytes: &axum::body::Bytes,
trace_id: &str,
decision: Option<&GatewayControlDecision>,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let Some(decision) = decision else {
return Ok(LocalExecutionRequestOutcome::NoPath);
};
#[cfg(not(test))]
{
if parts.method != http::Method::POST {
pub(crate) fn maybe_execute_sync_request<'a>(
state: &'a AppState,
parts: &'a http::request::Parts,
body_bytes: &'a axum::body::Bytes,
trace_id: &'a str,
decision: Option<&'a GatewayControlDecision>,
) -> Pin<Box<dyn Future<Output = Result<LocalExecutionRequestOutcome, GatewayError>> + Send + 'a>> {
Box::pin(async move {
let Some(decision) = decision else {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
return maybe_execute_sync_local_path(state, parts, body_bytes, trace_id, decision).await;
}
#[cfg(test)]
{
if state
.execution_runtime_override_base_url()
.unwrap_or_default()
.is_empty()
&& parts.method != http::Method::POST
};
#[cfg(not(test))]
{
return Ok(LocalExecutionRequestOutcome::NoPath);
if parts.method != http::Method::POST {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
return maybe_execute_sync_local_path(state, parts, body_bytes, trace_id, decision)
.await;
}
maybe_execute_sync_local_path(state, parts, body_bytes, trace_id, decision).await
}
#[cfg(test)]
{
if state
.execution_runtime_override_base_url()
.unwrap_or_default()
.is_empty()
&& parts.method != http::Method::POST
{
return Ok(LocalExecutionRequestOutcome::NoPath);
}
maybe_execute_sync_local_path(state, parts, body_bytes, trace_id, decision).await
}
})
}
pub(crate) async fn maybe_execute_stream_request(
state: &AppState,
parts: &http::request::Parts,
body_bytes: &axum::body::Bytes,
trace_id: &str,
decision: Option<&GatewayControlDecision>,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let Some(decision) = decision else {
return Ok(LocalExecutionRequestOutcome::NoPath);
};
#[cfg(not(test))]
{
if parts.method != http::Method::POST {
pub(crate) fn maybe_execute_stream_request<'a>(
state: &'a AppState,
parts: &'a http::request::Parts,
body_bytes: &'a axum::body::Bytes,
trace_id: &'a str,
decision: Option<&'a GatewayControlDecision>,
) -> Pin<Box<dyn Future<Output = Result<LocalExecutionRequestOutcome, GatewayError>> + Send + 'a>> {
Box::pin(async move {
let Some(decision) = decision else {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
return maybe_execute_stream_local_path(state, parts, body_bytes, trace_id, decision).await;
}
#[cfg(test)]
{
if state
.execution_runtime_override_base_url()
.unwrap_or_default()
.is_empty()
&& parts.method != http::Method::POST
};
#[cfg(not(test))]
{
return Ok(LocalExecutionRequestOutcome::NoPath);
if parts.method != http::Method::POST {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
return maybe_execute_stream_local_path(state, parts, body_bytes, trace_id, decision)
.await;
}
maybe_execute_stream_local_path(state, parts, body_bytes, trace_id, decision).await
}
#[cfg(test)]
{
if state
.execution_runtime_override_base_url()
.unwrap_or_default()
.is_empty()
&& parts.method != http::Method::POST
{
return Ok(LocalExecutionRequestOutcome::NoPath);
}
maybe_execute_stream_local_path(state, parts, body_bytes, trace_id, decision).await
}
})
}
pub(crate) fn planner_decision_action(action: &str) -> bool {
+183 -3
View File
@@ -9,7 +9,11 @@ use std::collections::BTreeMap;
use crate::ai_serving::api::{
is_matching_stream_request, resolve_execution_runtime_stream_plan_kind,
supports_stream_execution_decision_kind, AiStreamAttempt, OPENAI_VIDEO_CONTENT_PLAN_KIND,
supports_stream_execution_decision_kind, AiStreamAttempt, CLAUDE_CHAT_STREAM_PLAN_KIND,
CLAUDE_CLI_STREAM_PLAN_KIND, GEMINI_CHAT_STREAM_PLAN_KIND, GEMINI_CLI_STREAM_PLAN_KIND,
GEMINI_FILES_DOWNLOAD_PLAN_KIND, OPENAI_CHAT_STREAM_PLAN_KIND, OPENAI_IMAGE_STREAM_PLAN_KIND,
OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND, OPENAI_RESPONSES_STREAM_PLAN_KIND,
OPENAI_VIDEO_CONTENT_PLAN_KIND,
};
use crate::api::response::build_client_response_from_parts;
use crate::control::GatewayControlDecision;
@@ -34,24 +38,78 @@ pub(crate) async fn maybe_execute_via_stream_decision_path(
trace_id: &str,
decision: &GatewayControlDecision,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let plan_kind_started_at = std::time::Instant::now();
let Some(plan_kind) = resolve_execution_runtime_stream_plan_kind(parts, decision) else {
observe_gateway_stage_ms(
"frontdoor_stream_plan_kind",
plan_kind_started_at.elapsed().as_millis() as u64,
);
return Ok(LocalExecutionRequestOutcome::NoPath);
};
observe_gateway_stage_ms(
"frontdoor_stream_plan_kind",
plan_kind_started_at.elapsed().as_millis() as u64,
);
let parse_started_at = std::time::Instant::now();
let Some((body_json, body_base64)) = parse_local_request_body(parts, body_bytes) else {
observe_gateway_stage_ms(
"frontdoor_stream_parse",
parse_started_at.elapsed().as_millis() as u64,
);
return Ok(LocalExecutionRequestOutcome::NoPath);
};
observe_gateway_stage_ms(
"frontdoor_stream_parse",
parse_started_at.elapsed().as_millis() as u64,
);
if !is_matching_stream_request(plan_kind, parts, &body_json, body_base64.as_deref()) {
let match_started_at = std::time::Instant::now();
let stream_matches =
is_matching_stream_request(plan_kind, parts, &body_json, body_base64.as_deref());
observe_gateway_stage_ms(
"frontdoor_stream_match",
match_started_at.elapsed().as_millis() as u64,
);
if !stream_matches {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
let bypass_started_at = std::time::Instant::now();
let bypass_cache_key =
build_direct_plan_bypass_cache_key(plan_kind, parts, body_bytes, decision);
if should_skip_direct_plan(state, &bypass_cache_key) {
let skip_direct_plan = should_skip_direct_plan(state, &bypass_cache_key);
observe_gateway_stage_ms(
"frontdoor_stream_bypass",
bypass_started_at.elapsed().as_millis() as u64,
);
if skip_direct_plan {
return Ok(LocalExecutionRequestOutcome::NoPath);
}
if plan_kind == OPENAI_CHAT_STREAM_PLAN_KIND
&& supports_stream_execution_decision_kind(plan_kind)
&& decision.route_family.as_deref() == Some("openai")
{
let fast_path_started_at = std::time::Instant::now();
let outcome = execute_openai_chat_stream_fast_path(
state,
parts,
trace_id,
decision,
&body_json,
body_base64,
plan_kind,
bypass_cache_key,
)
.await;
observe_gateway_stage_ms(
"frontdoor_stream_fast_path_total",
fast_path_started_at.elapsed().as_millis() as u64,
);
return outcome;
}
let port = GatewayStreamExecutionPathPort {
state,
parts,
@@ -69,6 +127,64 @@ pub(crate) async fn maybe_execute_via_stream_decision_path(
))
}
#[allow(clippy::too_many_arguments)]
async fn execute_openai_chat_stream_fast_path(
state: &AppState,
parts: &http::request::Parts,
trace_id: &str,
decision: &GatewayControlDecision,
body_json: &serde_json::Value,
body_base64: Option<String>,
plan_kind: &str,
bypass_cache_key: String,
) -> Result<LocalExecutionRequestOutcome, GatewayError> {
let started_at = std::time::Instant::now();
let local_outcome = maybe_execute_stream_via_local_decision(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?;
observe_gateway_stage_ms(
"stream_openai_chat_local_decision",
started_at.elapsed().as_millis() as u64,
);
match local_outcome {
LocalExecutionRequestOutcome::Responded(response) => {
return Ok(LocalExecutionRequestOutcome::Responded(response));
}
LocalExecutionRequestOutcome::Exhausted(outcome) => {
return Ok(LocalExecutionRequestOutcome::Exhausted(outcome));
}
LocalExecutionRequestOutcome::NoPath => {}
}
if let Some(response) = maybe_execute_stream_via_remote_decision(
state, parts, trace_id, decision, body_json, plan_kind,
)
.await?
{
return Ok(LocalExecutionRequestOutcome::Responded(response));
}
let fallback_started_at = std::time::Instant::now();
let fallback_outcome = maybe_execute_stream_via_plan_fallback(
state,
parts,
trace_id,
decision,
body_json,
body_base64,
plan_kind,
bypass_cache_key,
GatewayFallbackReason::RemoteDecisionMiss,
)
.await?;
observe_gateway_stage_ms(
"frontdoor_stream_fast_path",
fallback_started_at.elapsed().as_millis() as u64,
);
Ok(fallback_outcome)
}
struct GatewayStreamExecutionPathPort<'a> {
state: &'a AppState,
parts: &'a http::request::Parts,
@@ -91,6 +207,10 @@ impl AiStreamExecutionPathPort for GatewayStreamExecutionPathPort<'_> {
self.scheduler_supported
}
fn stream_execution_steps(&self) -> &'static [AiStreamExecutionStep] {
stream_execution_steps_for_plan_kind(self.plan_kind)
}
async fn execute_stream_step(
&self,
step: AiStreamExecutionStep,
@@ -194,6 +314,10 @@ impl AiStreamExecutionPathPort for GatewayStreamExecutionPathPort<'_> {
"stream_path_step",
step_started_at.elapsed().as_millis() as u64,
);
observe_gateway_stage_ms(
stream_path_stage_name(step),
step_started_at.elapsed().as_millis() as u64,
);
Ok(to_ai_serving_outcome(outcome))
}
@@ -217,6 +341,62 @@ impl AiStreamExecutionPathPort for GatewayStreamExecutionPathPort<'_> {
}
}
const STREAM_STEPS_VIDEO_CONTENT: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalVideoContent,
AiStreamExecutionStep::RemoteDecision,
];
const STREAM_STEPS_OPENAI_IMAGE: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalImage,
AiStreamExecutionStep::RemoteDecision,
];
const STREAM_STEPS_OPENAI_CHAT: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalOpenAiChat,
AiStreamExecutionStep::RemoteDecision,
];
const STREAM_STEPS_OPENAI_RESPONSES: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalOpenAiResponses,
AiStreamExecutionStep::RemoteDecision,
];
const STREAM_STEPS_STANDARD_TEXT: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalStandardFamily,
AiStreamExecutionStep::LocalSameFormatProvider,
AiStreamExecutionStep::RemoteDecision,
];
const STREAM_STEPS_GEMINI_FILES: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalGeminiFiles,
AiStreamExecutionStep::RemoteDecision,
];
fn stream_execution_steps_for_plan_kind(plan_kind: &str) -> &'static [AiStreamExecutionStep] {
match plan_kind {
OPENAI_VIDEO_CONTENT_PLAN_KIND => STREAM_STEPS_VIDEO_CONTENT,
OPENAI_IMAGE_STREAM_PLAN_KIND => STREAM_STEPS_OPENAI_IMAGE,
OPENAI_CHAT_STREAM_PLAN_KIND => STREAM_STEPS_OPENAI_CHAT,
OPENAI_RESPONSES_STREAM_PLAN_KIND | OPENAI_RESPONSES_COMPACT_STREAM_PLAN_KIND => {
STREAM_STEPS_OPENAI_RESPONSES
}
CLAUDE_CHAT_STREAM_PLAN_KIND
| CLAUDE_CLI_STREAM_PLAN_KIND
| GEMINI_CHAT_STREAM_PLAN_KIND
| GEMINI_CLI_STREAM_PLAN_KIND => STREAM_STEPS_STANDARD_TEXT,
GEMINI_FILES_DOWNLOAD_PLAN_KIND => STREAM_STEPS_GEMINI_FILES,
_ => aether_ai_serving::DEFAULT_STREAM_EXECUTION_STEPS,
}
}
fn stream_path_stage_name(step: AiStreamExecutionStep) -> &'static str {
match step {
AiStreamExecutionStep::LocalVideoContent => "stream_path_step_video_content",
AiStreamExecutionStep::LocalImage => "stream_path_step_image",
AiStreamExecutionStep::LocalOpenAiChat => "stream_path_step_openai_chat",
AiStreamExecutionStep::LocalOpenAiResponses => "stream_path_step_openai_responses",
AiStreamExecutionStep::LocalStandardFamily => "stream_path_step_standard_family",
AiStreamExecutionStep::LocalSameFormatProvider => "stream_path_step_same_format_provider",
AiStreamExecutionStep::LocalGeminiFiles => "stream_path_step_gemini_files",
AiStreamExecutionStep::RemoteDecision => "stream_path_step_remote_decision",
}
}
fn to_ai_serving_outcome(
outcome: LocalExecutionRequestOutcome,
) -> AiServingExecutionOutcome<Response<Body>, super::LocalExecutionExhaustion> {
@@ -1039,13 +1039,20 @@ async fn proxy_request_inner(
request: Request,
) -> Result<Response<Body>, GatewayError> {
let started_at = Instant::now();
if let Some(accepted_at) = request
let accepted_at = request
.extensions()
.get::<crate::middleware::GatewayRequestAcceptedAt>()
.map(|accepted_at| accepted_at.0)
.unwrap_or(started_at);
crate::request_diagnostics::record_request_accepted_at(accepted_at);
if request
.extensions()
.get::<crate::middleware::GatewayRequestAcceptedAt>()
.is_some()
{
observe_gateway_stage_ms(
"frontdoor_handler_queue",
started_at.duration_since(accepted_at.0).as_millis() as u64,
started_at.duration_since(accepted_at).as_millis() as u64,
);
}
let mut request_permit = match state.try_acquire_request_permit().await {
+5 -1
View File
@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
#![allow(
dead_code,
unused_assignments,
@@ -73,6 +74,8 @@ mod stage_metrics;
mod state;
mod system_features;
mod task_runtime;
#[cfg(feature = "testkit")]
pub mod testkit;
mod tunnel;
mod upstream_admission;
mod usage;
@@ -96,7 +99,8 @@ pub(crate) use self::execution_runtime::{
};
pub use self::execution_runtime::{
build_execution_runtime_router, build_execution_runtime_router_with_request_concurrency_limit,
build_execution_runtime_router_with_request_gates, serve_execution_runtime_tcp,
build_execution_runtime_router_with_request_gates,
prewarm_direct_h2c_sender_cache_from_env_for_startup, serve_execution_runtime_tcp,
serve_execution_runtime_unix,
};
pub(crate) use self::fallback_metrics::{GatewayFallbackMetricKind, GatewayFallbackReason};
+178 -31
View File
@@ -1,11 +1,19 @@
#[cfg(not(target_env = "msvc"))]
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use std::path::PathBuf;
use std::sync::Arc;
use axum::{body::Body, extract::Request};
use clap::{Args as ClapArgs, Parser, Subcommand, ValueEnum};
use hyper::body::Incoming;
use hyper_util::{
rt::{TokioExecutor, TokioIo},
server::conn::auto::Builder as HyperServerBuilder,
service::TowerToHyperService,
};
use tower::{Service as _, ServiceExt as _};
use tracing::{debug, info, warn};
use aether_crypto::warm_python_fernet_secret;
@@ -15,7 +23,8 @@ use aether_data::lifecycle::export::{
};
use aether_data::{DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig, DEFAULT_SQLITE_DATABASE_URL};
use aether_gateway::{
attach_static_frontend, build_router_with_state, set_gateway_frontdoor_app_port, AppState,
attach_static_frontend, build_router_with_state,
prewarm_direct_h2c_sender_cache_from_env_for_startup, set_gateway_frontdoor_app_port, AppState,
FrontdoorCorsConfig, FrontdoorUserRpmConfig, GatewayDataConfig, UsageRuntimeConfig,
VideoTaskTruthSourceMode,
};
@@ -236,11 +245,14 @@ const AUTO_SERVER_SQL_POOL_MIN_CONNECTIONS_FLOOR: u32 = 4;
const AUTO_SERVER_SQL_POOL_MIN_CONNECTIONS_CAP: u32 = 16;
const AUTO_SERVER_SQL_POOL_MAX_CONNECTIONS_FLOOR: u32 = 20;
const AUTO_SERVER_SQL_POOL_MAX_CONNECTIONS_CAP: u32 = 100;
const DEFAULT_GATEWAY_LISTEN_BACKLOG: i32 = 8192;
const DEFAULT_GATEWAY_LISTEN_BACKLOG: i32 = 65_535;
const MIN_GATEWAY_LISTEN_BACKLOG: i32 = 128;
const MAX_GATEWAY_LISTEN_BACKLOG: i32 = 65_535;
const DEFAULT_GATEWAY_LISTENER_SHARDS: usize = 1;
const DEFAULT_GATEWAY_LISTENER_SHARDS: usize = 0;
const MAX_GATEWAY_LISTENER_SHARDS: usize = 64;
const DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS: u32 = 16_384;
const MIN_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS: u32 = 200;
const MAX_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS: u32 = 1_000_000;
fn env_var_trimmed(name: &str) -> Option<String> {
std::env::var(name)
.ok()
@@ -576,6 +588,27 @@ struct GatewayUsageArgs {
)]
queue_reclaim_interval_ms: u64,
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_TERMINAL_ENQUEUE_MAX_IN_FLIGHT",
default_value_t = 256
)]
terminal_enqueue_max_in_flight: u64,
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_LIFECYCLE_ENQUEUE_MAX_IN_FLIGHT",
default_value_t = 128
)]
lifecycle_enqueue_max_in_flight: u64,
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_RETRY_DEFERRED_LIFECYCLE_EVENTS",
default_value_t = false
)]
retry_deferred_lifecycle_events: bool,
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_ENQUEUE_RETRY_BUFFER_CAPACITY",
@@ -593,14 +626,14 @@ struct GatewayUsageArgs {
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_ENQUEUE_RETRY_INITIAL_BACKOFF_MS",
default_value_t = 10
default_value_t = 3_000
)]
enqueue_retry_initial_backoff_ms: u64,
#[arg(
long,
env = "AETHER_GATEWAY_USAGE_ENQUEUE_RETRY_MAX_BACKOFF_MS",
default_value_t = 1_000
default_value_t = 10_000
)]
enqueue_retry_max_backoff_ms: u64,
}
@@ -620,6 +653,9 @@ impl GatewayUsageArgs {
reclaim_idle_ms: self.queue_reclaim_idle_ms.max(1),
reclaim_count: self.queue_reclaim_count.max(1),
reclaim_interval_ms: self.queue_reclaim_interval_ms.max(1),
terminal_enqueue_max_in_flight: self.terminal_enqueue_max_in_flight.max(1),
lifecycle_enqueue_max_in_flight: self.lifecycle_enqueue_max_in_flight.max(1),
retry_deferred_lifecycle_events: self.retry_deferred_lifecycle_events,
enqueue_retry_buffer_capacity: self.enqueue_retry_buffer_capacity.max(1),
enqueue_retry_workers: self.enqueue_retry_workers.clamp(1, 64),
enqueue_retry_initial_backoff_ms: self.enqueue_retry_initial_backoff_ms.max(1),
@@ -806,8 +842,16 @@ struct Args {
env = "AETHER_GATEWAY_LISTENER_SHARDS",
default_value_t = DEFAULT_GATEWAY_LISTENER_SHARDS
)]
/// Number of SO_REUSEPORT listener shards. 0 selects a high-concurrency default.
listener_shards: usize,
#[arg(
long,
env = "AETHER_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS",
default_value_t = DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS
)]
http2_max_concurrent_streams: u32,
/// 容器内健康检查入口:根据当前 bind 端口探测本地 /health。
#[arg(long, hide = true, default_value_t = false)]
healthcheck: bool,
@@ -1061,10 +1105,36 @@ fn gateway_listen_backlog(backlog: i32) -> i32 {
backlog.clamp(MIN_GATEWAY_LISTEN_BACKLOG, MAX_GATEWAY_LISTEN_BACKLOG)
}
fn gateway_auto_listener_shards() -> usize {
#[cfg(unix)]
{
std::thread::available_parallelism()
.map(|parallelism| parallelism.get().saturating_mul(2))
.unwrap_or(16)
.clamp(8, 16)
.min(MAX_GATEWAY_LISTENER_SHARDS)
}
#[cfg(not(unix))]
{
1
}
}
fn gateway_listener_shards(shards: usize) -> usize {
if shards == 0 {
return gateway_auto_listener_shards();
}
shards.clamp(1, MAX_GATEWAY_LISTENER_SHARDS)
}
fn gateway_http2_max_concurrent_streams(streams: u32) -> u32 {
streams.clamp(
MIN_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS,
MAX_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS,
)
}
fn gateway_listener(
bind_addr: std::net::SocketAddr,
backlog: i32,
@@ -1115,29 +1185,15 @@ fn gateway_listeners(
async fn serve_gateway_router(
listeners: Vec<tokio::net::TcpListener>,
router: axum::Router,
http2_max_concurrent_streams: u32,
) -> Result<(), Box<dyn std::error::Error>> {
if listeners.len() == 1 {
let listener = listeners
.into_iter()
.next()
.ok_or_else(|| std::io::Error::other("gateway listener set is empty"))?;
axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await?;
return Ok(());
}
let http2_max_concurrent_streams =
gateway_http2_max_concurrent_streams(http2_max_concurrent_streams);
let mut servers = tokio::task::JoinSet::new();
for listener in listeners {
let router = router.clone();
servers.spawn(async move {
axum::serve(
listener,
router.into_make_service_with_connect_info::<std::net::SocketAddr>(),
)
.await
serve_gateway_listener(listener, router, http2_max_concurrent_streams).await
});
}
if let Some(result) = servers.join_next().await {
@@ -1149,6 +1205,38 @@ async fn serve_gateway_router(
Ok(())
}
async fn serve_gateway_listener(
listener: tokio::net::TcpListener,
router: axum::Router,
http2_max_concurrent_streams: u32,
) -> Result<(), std::io::Error> {
let mut make_service = router.into_make_service_with_connect_info::<std::net::SocketAddr>();
loop {
let (io, remote_addr) = listener.accept().await?;
let tower_service = make_service
.call(remote_addr)
.await
.unwrap_or_else(|err| match err {})
.map_request(|req: Request<Incoming>| req.map(Body::new));
let hyper_service = TowerToHyperService::new(tower_service);
let io = TokioIo::new(io);
tokio::spawn(async move {
let mut builder = HyperServerBuilder::new(TokioExecutor::new());
builder.http2().enable_connect_protocol();
builder
.http2()
.max_concurrent_streams(http2_max_concurrent_streams);
if let Err(err) = builder
.serve_connection_with_upgrades(io, hyper_service)
.await
{
tracing::trace!(error = ?err, "gateway connection closed with error");
}
});
}
}
fn resolve_local_http_base_url(app_port: u16) -> Result<String, std::io::Error> {
Ok(format!("http://127.0.0.1:{}", validate_app_port(app_port)?))
}
@@ -1482,6 +1570,33 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
);
}
}
match prewarm_direct_h2c_sender_cache_from_env_for_startup().await {
Ok(Some(report)) => {
if report.failed_targets > 0 {
warn!(
requested_urls = report.requested_urls,
unique_targets = report.unique_targets,
warmed_targets = report.warmed_targets,
failed_targets = report.failed_targets,
ready_required = report.ready_required,
first_error = ?report.first_error,
"direct h2c sender cache prewarm completed with failures"
);
} else {
info!(
requested_urls = report.requested_urls,
unique_targets = report.unique_targets,
warmed_targets = report.warmed_targets,
ready_required = report.ready_required,
"direct h2c sender cache prewarmed"
);
}
}
Ok(None) => {}
Err(err) => {
return Err(std::io::Error::other(err).into());
}
}
let background_tasks = if args.node_role.spawns_background_tasks() {
Some(state.spawn_background_tasks())
@@ -1516,13 +1631,14 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
app_port,
listen_backlog,
listener_shards,
http2_max_concurrent_streams = gateway_http2_max_concurrent_streams(args.http2_max_concurrent_streams),
public_url = %public_base_url,
healthcheck_url = %frontdoor_health_url,
legacy_route_policy = "fail_closed",
"aether-gateway ready"
);
serve_gateway_router(listeners, router).await?;
serve_gateway_router(listeners, router, args.http2_max_concurrent_streams).await?;
if let Some(background_tasks) = background_tasks {
background_tasks.shutdown().await;
}
@@ -1889,8 +2005,11 @@ mod tests {
DatabaseDriverArg, DeploymentTopologyArg, GatewayDataArgs, GatewayFrontdoorArgs,
GatewayLogDestinationArg, GatewayLogFormatArg, GatewayLogRotationArg, GatewayLoggingArgs,
GatewayRateLimitArgs, GatewayUsageArgs, NodeRoleArg, RuntimeBackendArg,
VideoTaskTruthSourceArg, DEFAULT_GATEWAY_LISTENER_SHARDS, DEFAULT_GATEWAY_LISTEN_BACKLOG,
MAX_GATEWAY_LISTENER_SHARDS, MAX_GATEWAY_LISTEN_BACKLOG, MIN_GATEWAY_LISTEN_BACKLOG,
VideoTaskTruthSourceArg, DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS,
DEFAULT_GATEWAY_LISTENER_SHARDS, DEFAULT_GATEWAY_LISTEN_BACKLOG,
MAX_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS, MAX_GATEWAY_LISTENER_SHARDS,
MAX_GATEWAY_LISTEN_BACKLOG, MIN_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS,
MIN_GATEWAY_LISTEN_BACKLOG,
};
use aether_data::{DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig};
use aether_gateway::AppState;
@@ -1901,6 +2020,7 @@ mod tests {
app_port: 8084,
listen_backlog: DEFAULT_GATEWAY_LISTEN_BACKLOG,
listener_shards: DEFAULT_GATEWAY_LISTENER_SHARDS,
http2_max_concurrent_streams: DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS,
healthcheck: false,
healthcheck_timeout_ms: 3_000,
deployment_topology: DeploymentTopologyArg::SingleNode,
@@ -1951,10 +2071,13 @@ mod tests {
queue_reclaim_idle_ms: 30_000,
queue_reclaim_count: 500,
queue_reclaim_interval_ms: 5_000,
terminal_enqueue_max_in_flight: 256,
lifecycle_enqueue_max_in_flight: 128,
retry_deferred_lifecycle_events: false,
enqueue_retry_buffer_capacity: 131_072,
enqueue_retry_workers: 4,
enqueue_retry_initial_backoff_ms: 10,
enqueue_retry_max_backoff_ms: 1_000,
enqueue_retry_initial_backoff_ms: 3_000,
enqueue_retry_max_backoff_ms: 10_000,
},
frontdoor: GatewayFrontdoorArgs {
environment: "development".to_string(),
@@ -2009,10 +2132,12 @@ mod tests {
#[test]
fn clamps_gateway_listener_shards() {
assert_eq!(super::gateway_listener_shards(0), 1);
let auto_shards = super::gateway_listener_shards(0);
assert!((1..=MAX_GATEWAY_LISTENER_SHARDS).contains(&auto_shards));
assert_eq!(super::gateway_listener_shards(1), 1);
assert_eq!(
super::gateway_listener_shards(DEFAULT_GATEWAY_LISTENER_SHARDS),
DEFAULT_GATEWAY_LISTENER_SHARDS
auto_shards
);
assert_eq!(
super::gateway_listener_shards(MAX_GATEWAY_LISTENER_SHARDS + 1),
@@ -2020,6 +2145,28 @@ mod tests {
);
}
#[test]
fn clamps_gateway_http2_max_concurrent_streams() {
assert_eq!(
super::gateway_http2_max_concurrent_streams(
MIN_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS - 1
),
MIN_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS
);
assert_eq!(
super::gateway_http2_max_concurrent_streams(
DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS
),
DEFAULT_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS
);
assert_eq!(
super::gateway_http2_max_concurrent_streams(
MAX_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS + 1
),
MAX_GATEWAY_HTTP2_MAX_CONCURRENT_STREAMS
);
}
#[test]
fn explicit_migrate_runtime_config_enables_data_logs() {
let mut args = test_args();
@@ -657,16 +657,10 @@ async fn record_health_failure_effect(
.or(current_key.circuit_breaker_by_format.as_ref())
};
if !provider_key_health_success_persist_gate_allows(
&context.plan.key_id,
api_format,
circuit_breaker_update_owned.is_some(),
) {
return;
}
provider_key_health_success_persist_gate_reset(&context.plan.key_id, api_format);
if let Err(err) = state
.update_provider_catalog_key_health_state(
.update_provider_catalog_key_success_health_state(
&context.plan.key_id,
current_key.is_active,
Some(&health_by_format),
@@ -731,7 +725,13 @@ async fn record_health_success_effect(
.or(current_key.circuit_breaker_by_format.as_ref())
};
clear_provider_key_health_success_persist_gate(&context.plan.key_id, api_format);
if !provider_key_health_success_persist_gate_allows(
&context.plan.key_id,
api_format,
circuit_breaker_update_owned.is_some(),
) {
return;
}
if let Err(err) = state
.update_provider_catalog_key_health_state(
@@ -770,7 +770,7 @@ fn provider_key_health_success_persist_gate_allows(
)
}
fn clear_provider_key_health_success_persist_gate(key_id: &str, api_format: &str) {
fn provider_key_health_success_persist_gate_reset(key_id: &str, api_format: &str) {
let key = provider_key_health_success_persist_gate_key(key_id, api_format);
HEALTH_SUCCESS_PERSIST_GATE.remove(&key);
}
+87 -13
View File
@@ -1,6 +1,6 @@
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
Arc, OnceLock,
};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -18,12 +18,20 @@ const PROVIDER_POOL_IN_FLIGHT_TOKEN_TTL_MS: u64 = 120_000;
const PROVIDER_POOL_IN_FLIGHT_RENEW_MS: u64 = 30_000;
const PROVIDER_POOL_IN_FLIGHT_ACQUIRE_TIMEOUT_ENV: &str =
"AETHER_GATEWAY_PROVIDER_POOL_IN_FLIGHT_ACQUIRE_TIMEOUT_MS";
const PROVIDER_POOL_IN_FLIGHT_MODE_ENV: &str = "AETHER_GATEWAY_PROVIDER_POOL_IN_FLIGHT_MODE";
const DEFAULT_PROVIDER_POOL_IN_FLIGHT_ACQUIRE_TIMEOUT_MS: u64 = 10;
const PROVIDER_POOL_DEMAND_SNAPSHOT_TTL_SECONDS: u64 = 6 * 60 * 60;
const PROVIDER_POOL_DEMAND_ALPHA: f64 = 0.2;
const PROVIDER_POOL_DEMAND_HEADROOM: f64 = 1.2;
const PROVIDER_POOL_DEMAND_FLOOR: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProviderPoolInFlightMode {
Local,
Runtime,
Off,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub(crate) struct ProviderPoolDemandSnapshot {
pub(crate) in_flight: usize,
@@ -210,7 +218,7 @@ fn token_expiry_score(now_ms: u64) -> f64 {
}
fn provider_pool_in_flight_acquire_timeout() -> Duration {
static TIMEOUT: std::sync::OnceLock<Duration> = std::sync::OnceLock::new();
static TIMEOUT: OnceLock<Duration> = OnceLock::new();
*TIMEOUT.get_or_init(|| {
let millis = std::env::var(PROVIDER_POOL_IN_FLIGHT_ACQUIRE_TIMEOUT_ENV)
.ok()
@@ -220,6 +228,25 @@ fn provider_pool_in_flight_acquire_timeout() -> Duration {
})
}
fn provider_pool_in_flight_mode() -> ProviderPoolInFlightMode {
static MODE: OnceLock<ProviderPoolInFlightMode> = OnceLock::new();
*MODE.get_or_init(|| {
parse_provider_pool_in_flight_mode(
std::env::var(PROVIDER_POOL_IN_FLIGHT_MODE_ENV)
.ok()
.as_deref(),
)
})
}
fn parse_provider_pool_in_flight_mode(value: Option<&str>) -> ProviderPoolInFlightMode {
match value.map(str::trim).map(str::to_ascii_lowercase).as_deref() {
Some("runtime" | "redis" | "distributed") => ProviderPoolInFlightMode::Runtime,
Some("off" | "none" | "false" | "0" | "disabled") => ProviderPoolInFlightMode::Off,
_ => ProviderPoolInFlightMode::Local,
}
}
fn spawn_in_flight_renewal(
runtime: Arc<RuntimeState>,
tokens_key: String,
@@ -260,15 +287,29 @@ pub(crate) async fn acquire_provider_pool_in_flight_guard(
return None;
}
if runtime.is_memory() {
let counter = increment_local_provider_in_flight(provider_id);
return Some(ProviderPoolInFlightGuard {
kind: ProviderPoolInFlightGuardKind::Local {
provider_id: provider_id.to_string(),
counter,
},
released: false,
});
match provider_pool_in_flight_mode() {
ProviderPoolInFlightMode::Off => return None,
ProviderPoolInFlightMode::Local => {
let counter = increment_local_provider_in_flight(provider_id);
return Some(ProviderPoolInFlightGuard {
kind: ProviderPoolInFlightGuardKind::Local {
provider_id: provider_id.to_string(),
counter,
},
released: false,
});
}
ProviderPoolInFlightMode::Runtime if runtime.is_memory() => {
let counter = increment_local_provider_in_flight(provider_id);
return Some(ProviderPoolInFlightGuard {
kind: ProviderPoolInFlightGuardKind::Local {
provider_id: provider_id.to_string(),
counter,
},
released: false,
});
}
ProviderPoolInFlightMode::Runtime => {}
}
let tokens_key = in_flight_tokens_key(provider_id);
@@ -326,8 +367,13 @@ pub(crate) async fn provider_pool_live_in_flight_count(
if provider_id.is_empty() {
return 0;
}
if runtime.is_memory() {
return local_provider_live_in_flight_count(provider_id);
match provider_pool_in_flight_mode() {
ProviderPoolInFlightMode::Off => return 0,
ProviderPoolInFlightMode::Local => return local_provider_live_in_flight_count(provider_id),
ProviderPoolInFlightMode::Runtime if runtime.is_memory() => {
return local_provider_live_in_flight_count(provider_id);
}
ProviderPoolInFlightMode::Runtime => {}
}
let key = in_flight_tokens_key(provider_id);
let now_ms = current_unix_ms() as f64;
@@ -477,6 +523,34 @@ mod tests {
use super::*;
use aether_runtime_state::{MemoryRuntimeStateConfig, RuntimeState};
#[test]
fn provider_pool_in_flight_mode_defaults_local_and_allows_runtime_or_off() {
assert_eq!(
parse_provider_pool_in_flight_mode(None),
ProviderPoolInFlightMode::Local
);
assert_eq!(
parse_provider_pool_in_flight_mode(Some("")),
ProviderPoolInFlightMode::Local
);
assert_eq!(
parse_provider_pool_in_flight_mode(Some("runtime")),
ProviderPoolInFlightMode::Runtime
);
assert_eq!(
parse_provider_pool_in_flight_mode(Some("redis")),
ProviderPoolInFlightMode::Runtime
);
assert_eq!(
parse_provider_pool_in_flight_mode(Some("off")),
ProviderPoolInFlightMode::Off
);
assert_eq!(
parse_provider_pool_in_flight_mode(Some("0")),
ProviderPoolInFlightMode::Off
);
}
#[tokio::test]
async fn in_flight_guard_tracks_and_releases_provider_tokens() {
let runtime = Arc::new(RuntimeState::memory(MemoryRuntimeStateConfig::default()));
+27 -5
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use aether_cache::ExpiringMap;
use aether_runtime_state::{RateLimitCheck, RateLimitInput, RateLimitScope};
@@ -9,6 +9,7 @@ use tokio::sync::Mutex;
use tracing::warn;
use crate::control::GatewayControlDecision;
use crate::stage_metrics::observe_gateway_stage_ms;
use crate::{AppState, GatewayError};
const SYSTEM_RPM_CONFIG_KEY: &str = "rate_limit_per_minute";
@@ -150,8 +151,12 @@ impl FrontdoorUserRpmLimiter {
scope_key: &str,
bucket: u64,
) -> Result<u32, GatewayError> {
match state.runtime_state.kv_get(scope_key).await {
Ok(raw) => return Ok(raw.and_then(|value| value.parse::<u32>().ok()).unwrap_or(0)),
match state
.runtime_state
.rate_limit_count(scope_key, bucket)
.await
{
Ok(count) => return Ok(count),
Err(err) if !self.config.allow_local_fallback() => {
return Err(GatewayError::Internal(format!(
"frontdoor user rpm runtime read failed: {err}"
@@ -186,7 +191,12 @@ impl FrontdoorUserRpmLimiter {
return Ok(FrontdoorUserRpmOutcome::NotApplicable);
}
let system_default_started_at = Instant::now();
let system_default_limit = self.resolve_system_default_limit(state).await?;
observe_gateway_stage_ms(
"frontdoor_rpm_system_default",
system_default_started_at.elapsed().as_millis() as u64,
);
let Some(plan) = RpmPlan::from_decision(decision, &self.config, system_default_limit)
else {
return Ok(FrontdoorUserRpmOutcome::NotApplicable);
@@ -196,7 +206,13 @@ impl FrontdoorUserRpmLimiter {
return Ok(FrontdoorUserRpmOutcome::Allowed);
}
match self.check_and_consume_runtime(state, &plan).await {
let runtime_check_started_at = Instant::now();
let runtime_check = self.check_and_consume_runtime(state, &plan).await;
observe_gateway_stage_ms(
"frontdoor_rpm_runtime_check",
runtime_check_started_at.elapsed().as_millis() as u64,
);
match runtime_check {
Ok(outcome) => return Ok(outcome),
Err(err) => {
warn!(
@@ -226,7 +242,13 @@ impl FrontdoorUserRpmLimiter {
));
}
Ok(self.check_and_consume_memory(&plan).await)
let memory_fallback_started_at = Instant::now();
let outcome = self.check_and_consume_memory(&plan).await;
observe_gateway_stage_ms(
"frontdoor_rpm_memory_fallback",
memory_fallback_started_at.elapsed().as_millis() as u64,
);
Ok(outcome)
}
async fn resolve_system_default_limit(&self, state: &AppState) -> Result<u32, GatewayError> {
@@ -17,6 +17,7 @@ pub(crate) struct RequestDiagnostics {
#[derive(Debug, Default)]
struct RequestDiagnosticsInner {
request_accepted_at: Option<Instant>,
db_operations: BTreeMap<&'static str, DbOperationTiming>,
db_pool: Option<DbPoolObservation>,
}
@@ -38,6 +39,20 @@ struct DbPoolObservation {
}
impl RequestDiagnostics {
fn record_request_accepted_at(&self, accepted_at: Instant) {
let Ok(mut inner) = self.inner.lock() else {
return;
};
inner.request_accepted_at = Some(accepted_at);
}
pub(crate) fn request_accepted_at(&self) -> Option<Instant> {
let Ok(inner) = self.inner.lock() else {
return None;
};
inner.request_accepted_at
}
fn record_db_timing_ms(&self, operation: &'static str, elapsed_ms: u64) {
let Ok(mut inner) = self.inner.lock() else {
return;
@@ -145,6 +160,12 @@ pub(crate) fn current_request_diagnostics() -> Option<Arc<RequestDiagnostics>> {
REQUEST_DIAGNOSTICS.try_with(Arc::clone).ok()
}
pub(crate) fn record_request_accepted_at(accepted_at: Instant) {
if let Some(diagnostics) = current_request_diagnostics() {
diagnostics.record_request_accepted_at(accepted_at);
}
}
pub(crate) async fn observe_db_operation<F>(
operation: &'static str,
pool_summary: Option<DatabasePoolSummary>,
+4 -1
View File
@@ -4,7 +4,10 @@ pub(crate) mod selection;
pub(crate) mod trace;
pub(crate) use mutations::apply_routing_mutation_plan;
pub(crate) use resolver::{resolve_gateway_routing_policy, GatewayRoutingPolicyInput};
pub(crate) use resolver::{
resolve_gateway_routing_policy, resolve_gateway_static_default_routing_policy,
GatewayRoutingPolicyInput, GatewayStaticRoutingPolicyInput,
};
pub(crate) use selection::{
select_gateway_routing_group, GatewayRoutingGroupSelection, GatewayRoutingSelectionError,
GatewayRoutingSelectionInput, ROUTING_GROUP_HEADER,
+220 -2
View File
@@ -1,9 +1,11 @@
use aether_routing_core::{
resolve_routing_policy, ResolvedRoutingPolicy, RoutingGroupConfig, RoutingPolicyInput,
RoutingRulePhase,
resolve_routing_policy, MutationPlan, RankingOverlay, ResolvedRoutingPolicy,
RoutingGroupConfig, RoutingPolicyInput, RoutingRulePhase, RoutingSchedulingMode,
RoutingSetPriorityMode,
};
use http::StatusCode;
use serde_json::Value;
use std::collections::BTreeMap;
use crate::GatewayError;
@@ -23,9 +25,32 @@ pub(crate) struct GatewayRoutingPolicyInput<'a> {
pub phase: RoutingRulePhase,
}
#[derive(Debug, Clone)]
pub(crate) struct GatewayStaticRoutingPolicyInput<'a> {
pub group_id: Option<&'a str>,
pub group_version: Option<i64>,
pub group_config_json: &'a Value,
pub selection_source: &'a str,
pub requested_model: &'a str,
pub resolved_model: &'a str,
}
pub(crate) fn resolve_gateway_routing_policy(
input: GatewayRoutingPolicyInput<'_>,
) -> Result<ResolvedRoutingPolicy, GatewayError> {
if let Some(policy) =
resolve_gateway_static_default_routing_policy(GatewayStaticRoutingPolicyInput {
group_id: input.group_id,
group_version: input.group_version,
group_config_json: input.group_config_json,
selection_source: input.selection_source,
requested_model: input.requested_model,
resolved_model: input.resolved_model,
})?
{
return Ok(policy);
}
let config = serde_json::from_value::<RoutingGroupConfig>(input.group_config_json.clone())
.map_err(|err| GatewayError::Client {
status: StatusCode::BAD_REQUEST,
@@ -52,3 +77,196 @@ pub(crate) fn resolve_gateway_routing_policy(
message: err.to_string(),
})
}
pub(crate) fn resolve_gateway_static_default_routing_policy(
input: GatewayStaticRoutingPolicyInput<'_>,
) -> Result<Option<ResolvedRoutingPolicy>, GatewayError> {
let Some((priority_mode, scheduling_mode, keep_priority_on_conversion)) =
static_default_policy_fields(input.group_config_json)?
else {
return Ok(None);
};
Ok(Some(ResolvedRoutingPolicy {
group_id: input.group_id.map(str::to_string),
group_version: input.group_version,
selection_source: input.selection_source.to_string(),
requested_model: input.requested_model.to_string(),
resolved_model: input.resolved_model.to_string(),
priority_mode,
scheduling_mode,
keep_priority_on_conversion,
ranking_overlay: RankingOverlay::default(),
mutation_plan: MutationPlan::default(),
pool_policy_overrides: BTreeMap::new(),
matched_rules: Vec::new(),
}))
}
fn static_default_policy_fields(
config_json: &Value,
) -> Result<Option<(RoutingSetPriorityMode, RoutingSchedulingMode, bool)>, GatewayError> {
let Some(object) = config_json.as_object() else {
return Ok(None);
};
if !routing_array_field_is_missing_or_empty(object, "allowed_models")
|| !routing_array_field_is_missing_or_empty(object, "model_policies")
|| !routing_array_field_is_missing_or_empty(object, "rules")
{
return Ok(None);
}
let Some(default_policy) = object.get("default_policy") else {
return Ok(Some((
RoutingSetPriorityMode::default(),
RoutingSchedulingMode::default(),
false,
)));
};
let Some(default_policy) = default_policy.as_object() else {
return Ok(None);
};
let priority_mode = routing_enum_field(
default_policy.get("priority_mode"),
RoutingSetPriorityMode::default,
)?;
let scheduling_mode = routing_enum_field(
default_policy.get("scheduling_mode"),
RoutingSchedulingMode::default,
)?;
let keep_priority_on_conversion = match default_policy.get("keep_priority_on_conversion") {
Some(value) => value.as_bool().ok_or_else(|| {
invalid_routing_group_config("keep_priority_on_conversion must be a boolean")
})?,
None => false,
};
Ok(Some((
priority_mode,
scheduling_mode,
keep_priority_on_conversion,
)))
}
fn routing_array_field_is_missing_or_empty(
object: &serde_json::Map<String, Value>,
key: &str,
) -> bool {
match object.get(key) {
None => true,
Some(Value::Array(values)) => values.is_empty(),
Some(_) => false,
}
}
fn routing_enum_field<T>(
value: Option<&Value>,
default: impl FnOnce() -> T,
) -> Result<T, GatewayError>
where
T: serde::de::DeserializeOwned,
{
match value {
Some(value) => serde_json::from_value(value.clone()).map_err(|err| {
invalid_routing_group_config(format!("invalid default routing policy: {err}"))
}),
None => Ok(default()),
}
}
fn invalid_routing_group_config(message: impl Into<String>) -> GatewayError {
GatewayError::Client {
status: StatusCode::BAD_REQUEST,
message: format!("invalid routing group config: {}", message.into()),
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn static_default_policy_matches_full_resolver_without_body_context() {
let config = json!({
"default_policy": {
"priority_mode": "global_key",
"scheduling_mode": "load_balance",
"keep_priority_on_conversion": true
},
"allowed_models": [],
"model_policies": [],
"rules": []
});
let static_policy =
resolve_gateway_static_default_routing_policy(GatewayStaticRoutingPolicyInput {
group_id: Some("group-1"),
group_version: Some(7),
group_config_json: &config,
selection_source: "system_default",
requested_model: "mock-model",
resolved_model: "mock-model",
})
.expect("static default policy should resolve")
.expect("static default policy should be detected");
let full_policy = resolve_gateway_routing_policy(GatewayRoutingPolicyInput {
group_id: Some("group-1"),
group_version: Some(7),
group_config_json: &config,
selection_source: "system_default",
requested_model: "mock-model",
resolved_model: "mock-model",
api_format: "openai:chat",
user_id: Some("user-1"),
api_key_id: Some("key-1"),
headers: &json!({"x-test": "value"}),
body: &json!({"model": "mock-model"}),
phase: RoutingRulePhase::ClientRequest,
})
.expect("full policy should resolve");
assert_eq!(static_policy, full_policy);
assert_eq!(
static_policy.priority_mode,
RoutingSetPriorityMode::GlobalKey
);
assert_eq!(
static_policy.scheduling_mode,
RoutingSchedulingMode::LoadBalance
);
assert!(static_policy.keep_priority_on_conversion);
assert!(static_policy.mutation_plan.is_empty());
assert!(static_policy.matched_rules.is_empty());
}
#[test]
fn dynamic_routing_config_is_not_static_default() {
let config = json!({
"rules": [{
"id": "rule-1",
"conditions": {},
"actions": [{
"type": "restrict_providers",
"provider_ids": ["provider-1"]
}]
}]
});
let policy =
resolve_gateway_static_default_routing_policy(GatewayStaticRoutingPolicyInput {
group_id: Some("group-1"),
group_version: Some(1),
group_config_json: &config,
selection_source: "system_default",
requested_model: "mock-model",
resolved_model: "mock-model",
})
.expect("dynamic config should not fail static detection");
assert!(policy.is_none());
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ pub(crate) struct GatewayRoutingSelectionInput<'a> {
pub user_group_ids: &'a [String],
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct GatewayRoutingGroupSelection {
pub group: Option<StoredRoutingGroup>,
pub source: String,
@@ -17,9 +17,6 @@ pub(super) fn build_scheduler_affinity_cache_key(
global_model_name: &str,
client_session_affinity: Option<&ClientSessionAffinity>,
) -> Option<String> {
if !has_explicit_session_affinity(client_session_affinity) {
return None;
}
let api_key_id = auth_snapshot
.map(|snapshot| snapshot.api_key_id.trim())
.filter(|value| !value.is_empty())?;
@@ -225,6 +225,7 @@ pub(super) async fn collect_selectable_enumerated_candidates_with_skip_reasons(
);
let cached_affinity_target = if ordering_config.scheduling_mode
== SchedulerSchedulingMode::CacheAffinity
&& has_explicit_session_affinity(client_session_affinity)
{
affinity_cache_key.as_deref().and_then(|cache_key| {
runtime_state.read_cached_scheduler_affinity_target(cache_key, SCHEDULER_AFFINITY_TTL)
@@ -36,6 +36,7 @@ async fn select_candidate(
global_model_name: &str,
require_streaming: bool,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
client_session_affinity: Option<&ClientSessionAffinity>,
now_unix_secs: u64,
) -> Result<Option<SchedulerMinimalCandidateSelectionCandidate>, GatewayError> {
select_candidate_impl(
@@ -46,7 +47,7 @@ async fn select_candidate(
require_streaming,
None,
auth_snapshot,
None,
client_session_affinity,
now_unix_secs,
false,
)
@@ -207,9 +208,14 @@ async fn reuses_cached_scheduler_affinity_candidate_before_sorted_fallback() {
);
let auth_snapshot = sample_auth_snapshot("affinity-key-1");
let cache_key =
build_scheduler_affinity_cache_key(Some(&auth_snapshot), "openai:chat", "gpt-4.1", None)
.expect("cache key should build");
let client_session_affinity = ClientSessionAffinity::from_session_key("session-1");
let cache_key = build_scheduler_affinity_cache_key(
Some(&auth_snapshot),
"openai:chat",
"gpt-4.1",
Some(&client_session_affinity),
)
.expect("cache key should build");
state.remember_scheduler_affinity_target(
&cache_key,
SchedulerAffinityTarget {
@@ -228,6 +234,7 @@ async fn reuses_cached_scheduler_affinity_candidate_before_sorted_fallback() {
"gpt-4.1",
false,
Some(&auth_snapshot),
Some(&client_session_affinity),
100,
)
.await
@@ -312,9 +319,14 @@ async fn cached_affinity_candidate_cannot_use_reserved_provider_key_rpm_capacity
);
let auth_snapshot = sample_auth_snapshot("api-key-cached-user");
let cache_key =
build_scheduler_affinity_cache_key(Some(&auth_snapshot), "openai:chat", "gpt-4.1", None)
.expect("cache key should build");
let client_session_affinity = ClientSessionAffinity::from_session_key("session-1");
let cache_key = build_scheduler_affinity_cache_key(
Some(&auth_snapshot),
"openai:chat",
"gpt-4.1",
Some(&client_session_affinity),
)
.expect("cache key should build");
state.remember_scheduler_affinity_target(
&cache_key,
SchedulerAffinityTarget {
@@ -333,6 +345,7 @@ async fn cached_affinity_candidate_cannot_use_reserved_provider_key_rpm_capacity
"gpt-4.1",
false,
Some(&auth_snapshot),
Some(&client_session_affinity),
100,
)
.await
+163 -3
View File
@@ -6,7 +6,7 @@ use serde_json::{Map, Value};
const BUCKETS_MS: [u64; 12] = [1, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000];
const STAGES: [&str; 56] = [
const STAGES: &[&str] = &[
"frontdoor_handler_queue",
"frontdoor_admission",
"frontdoor_context",
@@ -14,11 +14,31 @@ const STAGES: [&str; 56] = [
"frontdoor_owner_forward",
"frontdoor_auth_model",
"frontdoor_rpm",
"frontdoor_rpm_system_default",
"frontdoor_rpm_runtime_check",
"frontdoor_rpm_memory_fallback",
"frontdoor_local_ai_public",
"frontdoor_execute_stream",
"frontdoor_stream_plan_kind",
"frontdoor_stream_parse",
"frontdoor_stream_match",
"frontdoor_stream_bypass",
"frontdoor_stream_fast_path",
"frontdoor_stream_fast_path_total",
"frontdoor_to_stream_response_ready",
"frontdoor_to_stream_body_first_poll",
"frontdoor_to_stream_first_client_yield",
"frontdoor_execute_sync",
"stream_candidate_slot",
"stream_path_step",
"stream_path_step_video_content",
"stream_path_step_image",
"stream_path_step_openai_chat",
"stream_path_step_openai_responses",
"stream_path_step_standard_family",
"stream_path_step_same_format_provider",
"stream_path_step_gemini_files",
"stream_path_step_remote_decision",
"stream_candidate_next",
"stream_candidate_source_next",
"stream_candidate_plan_build",
@@ -26,6 +46,17 @@ const STAGES: [&str; 56] = [
"stream_candidate_proxy",
"stream_candidate_report_context",
"stream_candidate_decision_build",
"openai_chat_decision_input_auth",
"openai_chat_decision_input_affinity",
"openai_chat_decision_input_routing",
"routing_user_groups_lookup",
"routing_group_selection",
"routing_group_selection_load",
"routing_static_policy_resolve",
"routing_policy_resolve",
"routing_mutation_apply",
"openai_chat_attempt_source_build",
"openai_chat_stream_target_select",
"openai_chat_payload_parts_prepare",
"openai_chat_payload_model_directives",
"openai_chat_payload_redaction",
@@ -47,7 +78,13 @@ const STAGES: [&str; 56] = [
"candidate_resolution_rank",
"direct_reqwest_client_prewarm",
"stream_candidate_execute",
"stream_candidate_watchdog_inline",
"stream_candidate_unused",
"stream_openai_chat_local_decision",
"stream_openai_chat_attempt_source_init",
"stream_openai_chat_attempt_source_execute",
"upstream_execution_gate_wait",
"upstream_execution_gate_held",
"stream_usage_pending",
"stream_provider_in_flight",
"stream_upstream_target_admission",
@@ -56,16 +93,38 @@ const STAGES: [&str; 56] = [
"stream_first_data",
"stream_response_policy",
"stream_response_ready",
"stream_response_build",
"stream_body_inline_first_poll",
"stream_first_client_yield",
"stream_upstream_target_permit_release",
"stream_finalizer_enqueue",
"stream_total",
"direct_passthrough_upstream_body_first",
"direct_passthrough_first_client_send",
"direct_passthrough_first_client_send_wait",
"direct_passthrough_body_send_wait",
"direct_passthrough_drop_drain_enqueue",
"direct_passthrough_drop_drain_shed",
"direct_passthrough_body_recv_first",
"direct_build_body",
"direct_request_prepare",
"direct_h2c_client_select",
"direct_h2c_request_build",
"direct_h2c_sender_ready_wait",
"direct_h2c_request_dispatch",
"direct_h2c_response_headers_wait",
"direct_h2c_request_headers_wait",
"direct_h2c_request_send",
"direct_reqwest_client_select",
"direct_reqwest_client_cache_lock",
"direct_reqwest_client_cache_warm_enqueue",
"direct_reqwest_request_build",
"direct_reqwest_request_send",
"direct_send_headers",
"candidate_planning_gate_wait",
];
const TRACE_STAGE_CAPACITY: usize = 16;
const TRACE_STAGE_CAPACITY: usize = 32;
const STAGE_TRACE_MODE_ENV: &str = "AETHER_GATEWAY_STAGE_TRACE_MODE";
const STAGE_TRACE_SLOW_MS_ENV: &str = "AETHER_GATEWAY_STAGE_TRACE_SLOW_MS";
const STAGE_TRACE_SAMPLE_RATE_ENV: &str = "AETHER_GATEWAY_STAGE_TRACE_SAMPLE_RATE";
@@ -76,6 +135,15 @@ static METRICS: LazyLock<Vec<StageMetric>> =
static STAGE_TRACE_CONFIG: LazyLock<RequestStageTraceConfig> =
LazyLock::new(read_stage_trace_config);
static STAGE_TRACE_SAMPLE_COUNTER: AtomicU64 = AtomicU64::new(0);
static STREAM_PRE_FIRST_BYTE_SPAWN_TOTAL: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_STREAM_RAW_TARGET_SELECT_SCANNED_TOTAL: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_STREAM_PAYLOAD_BUILD_SELECTED_TOTAL: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_STREAM_PAYLOAD_BUILD_PREFETCH_AVOIDED_TOTAL: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_STREAM_TARGET_SELECT_SELECTED_RANK_SUM: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_HIT_TOTAL: AtomicU64 = AtomicU64::new(0);
static OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_MISS_TOTAL: AtomicU64 = AtomicU64::new(0);
static CHAT_PII_REDACTION_REQUEST_CACHE_HIT_TOTAL: AtomicU64 = AtomicU64::new(0);
static CHAT_PII_REDACTION_REQUEST_CACHE_MISS_TOTAL: AtomicU64 = AtomicU64::new(0);
struct StageMetric {
stage: &'static str,
@@ -157,7 +225,99 @@ pub(crate) fn observe_gateway_stage_ms(stage: &'static str, elapsed_ms: u64) {
}
pub(crate) fn gateway_stage_metric_samples() -> Vec<MetricSample> {
METRICS.iter().flat_map(StageMetric::samples).collect()
let mut samples: Vec<MetricSample> = METRICS.iter().flat_map(StageMetric::samples).collect();
samples.push(MetricSample::new(
"stream_pre_first_byte_spawn_total",
"Number of per-request tasks spawned before the first client-visible stream byte.",
MetricKind::Counter,
STREAM_PRE_FIRST_BYTE_SPAWN_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_stream_target_select_raw_candidates_scanned_total",
"Number of raw OpenAI chat stream candidates inspected by lightweight target selection.",
MetricKind::Counter,
OPENAI_CHAT_STREAM_RAW_TARGET_SELECT_SCANNED_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_stream_payload_build_selected_total",
"Number of selected OpenAI chat stream raw candidates that entered full payload build.",
MetricKind::Counter,
OPENAI_CHAT_STREAM_PAYLOAD_BUILD_SELECTED_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_stream_payload_build_prefetch_avoided_total",
"Number of OpenAI chat stream candidate payload builds avoided during target-selection prefetch.",
MetricKind::Counter,
OPENAI_CHAT_STREAM_PAYLOAD_BUILD_PREFETCH_AVOIDED_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_stream_target_select_selected_rank_sum",
"Sum of zero-based selected candidate ranks within OpenAI chat stream target-selection windows.",
MetricKind::Counter,
OPENAI_CHAT_STREAM_TARGET_SELECT_SELECTED_RANK_SUM.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_model_directive_cache_hit_total",
"Number of per-request OpenAI chat model directive cache hits.",
MetricKind::Counter,
OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_HIT_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"openai_chat_model_directive_cache_miss_total",
"Number of per-request OpenAI chat model directive cache misses.",
MetricKind::Counter,
OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_MISS_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"chat_pii_redaction_request_cache_hit_total",
"Number of chat PII redaction request-cache hits.",
MetricKind::Counter,
CHAT_PII_REDACTION_REQUEST_CACHE_HIT_TOTAL.load(Ordering::Relaxed),
));
samples.push(MetricSample::new(
"chat_pii_redaction_request_cache_miss_total",
"Number of chat PII redaction request-cache misses.",
MetricKind::Counter,
CHAT_PII_REDACTION_REQUEST_CACHE_MISS_TOTAL.load(Ordering::Relaxed),
));
samples
}
pub(crate) fn record_stream_pre_first_byte_spawn() {
STREAM_PRE_FIRST_BYTE_SPAWN_TOTAL.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_stream_raw_candidates_scanned(count: usize) {
OPENAI_CHAT_STREAM_RAW_TARGET_SELECT_SCANNED_TOTAL.fetch_add(count as u64, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_stream_payload_build_selected() {
OPENAI_CHAT_STREAM_PAYLOAD_BUILD_SELECTED_TOTAL.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_stream_payload_build_prefetch_avoided(count: usize) {
OPENAI_CHAT_STREAM_PAYLOAD_BUILD_PREFETCH_AVOIDED_TOTAL
.fetch_add(count as u64, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_stream_target_select_selected_rank(rank: usize) {
OPENAI_CHAT_STREAM_TARGET_SELECT_SELECTED_RANK_SUM.fetch_add(rank as u64, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_model_directive_cache_hit() {
OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_HIT_TOTAL.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_openai_chat_model_directive_cache_miss() {
OPENAI_CHAT_MODEL_DIRECTIVE_CACHE_MISS_TOTAL.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_chat_pii_redaction_request_cache_hit() {
CHAT_PII_REDACTION_REQUEST_CACHE_HIT_TOTAL.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_chat_pii_redaction_request_cache_miss() {
CHAT_PII_REDACTION_REQUEST_CACHE_MISS_TOTAL.fetch_add(1, Ordering::Relaxed);
}
fn update_max(max: &AtomicU64, value: u64) {
+248 -23
View File
@@ -2,13 +2,15 @@ use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::RwLock as StdRwLock;
use std::time::Duration;
use aether_data::repository::users::StoredUserGroup;
use aether_data_contracts::repository::billing::UserDailyQuotaAvailabilityRecord;
use aether_data_contracts::repository::quota::StoredProviderQuotaSnapshot;
use aether_runtime::ConcurrencyGate;
use aether_runtime_state::{RuntimeSemaphore, RuntimeState};
use dashmap::DashMap;
use tokio::sync::Mutex as TokioMutex;
use super::super::async_task::{VideoTaskPollerConfig, VideoTaskService};
use super::super::cache::{
@@ -39,14 +41,25 @@ const MAX_LOCAL_EXECUTION_PLANNING_TIMEOUT_MS: u64 = 120_000;
const LOCAL_EXECUTION_PLANNING_TIMEOUT_MS_ENV: &str =
"AETHER_GATEWAY_LOCAL_EXECUTION_PLANNING_TIMEOUT_MS";
const DEFAULT_CANDIDATE_PLANNING_GATE_LIMIT: usize = 1024;
const DEFAULT_UPSTREAM_EXECUTION_GATE_LIMIT: usize = 2000;
const DEFAULT_UPSTREAM_TARGET_GATE_LIMIT: usize = 2000;
const DEFAULT_UPSTREAM_EXECUTION_GATE_LIMIT: usize = 10_000;
const DEFAULT_UPSTREAM_TARGET_GATE_LIMIT: usize = 10_000;
const MAX_CANDIDATE_PLANNING_GATE_LIMIT: usize = 8192;
const MAX_UPSTREAM_EXECUTION_GATE_LIMIT: usize = 16_384;
const MAX_UPSTREAM_TARGET_GATE_LIMIT: usize = 16_384;
const CANDIDATE_PLANNING_GATE_LIMIT_PER_CPU: usize = 256;
const UPSTREAM_EXECUTION_GATE_LIMIT_PER_CPU: usize = 1024;
const UPSTREAM_TARGET_GATE_LIMIT_PER_CPU: usize = 1024;
const GATE_LIMIT_FD_RESERVE: usize = 128;
const DEFAULT_INTERNAL_GATE_QUEUE_BUDGET_MS: u64 = 250;
const MAX_INTERNAL_GATE_QUEUE_BUDGET_MS: u64 = 5_000;
const CANDIDATE_PLANNING_GATE_LIMIT_ENV: &str = "AETHER_GATEWAY_CANDIDATE_PLANNING_GATE_LIMIT";
const UPSTREAM_EXECUTION_GATE_LIMIT_ENV: &str = "AETHER_GATEWAY_UPSTREAM_EXECUTION_GATE_LIMIT";
const UPSTREAM_TARGET_GATE_LIMIT_ENV: &str = "AETHER_GATEWAY_UPSTREAM_TARGET_GATE_LIMIT";
const INTERNAL_GATE_QUEUE_BUDGET_MS_ENV: &str = "AETHER_GATEWAY_INTERNAL_GATE_QUEUE_BUDGET_MS";
const DEFAULT_AUTH_CAPACITY_CACHE_TTL_MS: u64 = 500;
const MIN_AUTH_CAPACITY_CACHE_TTL_MS: u64 = 10;
const MAX_AUTH_CAPACITY_CACHE_TTL_MS: u64 = 10_000;
const AUTH_CAPACITY_CACHE_TTL_MS_ENV: &str = "AETHER_GATEWAY_AUTH_CAPACITY_CACHE_TTL_MS";
#[cfg(test)]
type TestExecutionRuntimeSyncOverrideFn = dyn Fn(
@@ -73,6 +86,7 @@ pub(crate) struct FrontdoorRuntimeGuardConfig {
pub(crate) request_body_read_timeout: Duration,
pub(crate) local_execution_planning_timeout: Duration,
pub(crate) internal_gate_queue_budget: Duration,
pub(crate) auth_capacity_cache_ttl: Duration,
pub(crate) candidate_planning_gate_limit: Option<usize>,
pub(crate) upstream_execution_gate_limit: Option<usize>,
pub(crate) upstream_target_gate_limit: Option<usize>,
@@ -99,18 +113,15 @@ impl FrontdoorRuntimeGuardConfig {
1,
MAX_INTERNAL_GATE_QUEUE_BUDGET_MS,
),
candidate_planning_gate_limit: env_optional_usize(
CANDIDATE_PLANNING_GATE_LIMIT_ENV,
DEFAULT_CANDIDATE_PLANNING_GATE_LIMIT,
),
upstream_execution_gate_limit: env_optional_usize(
UPSTREAM_EXECUTION_GATE_LIMIT_ENV,
DEFAULT_UPSTREAM_EXECUTION_GATE_LIMIT,
),
upstream_target_gate_limit: env_optional_usize(
UPSTREAM_TARGET_GATE_LIMIT_ENV,
DEFAULT_UPSTREAM_TARGET_GATE_LIMIT,
auth_capacity_cache_ttl: env_cache_duration_ms(
AUTH_CAPACITY_CACHE_TTL_MS_ENV,
DEFAULT_AUTH_CAPACITY_CACHE_TTL_MS,
MIN_AUTH_CAPACITY_CACHE_TTL_MS,
MAX_AUTH_CAPACITY_CACHE_TTL_MS,
),
candidate_planning_gate_limit: candidate_planning_gate_limit_from_env(),
upstream_execution_gate_limit: upstream_execution_gate_limit_from_env(),
upstream_target_gate_limit: upstream_target_gate_limit_from_env(),
}
}
@@ -125,6 +136,7 @@ impl FrontdoorRuntimeGuardConfig {
internal_gate_queue_budget: Duration::from_millis(
DEFAULT_INTERNAL_GATE_QUEUE_BUDGET_MS,
),
auth_capacity_cache_ttl: Duration::from_millis(DEFAULT_AUTH_CAPACITY_CACHE_TTL_MS),
candidate_planning_gate_limit: Some(DEFAULT_CANDIDATE_PLANNING_GATE_LIMIT),
upstream_execution_gate_limit: Some(DEFAULT_UPSTREAM_EXECUTION_GATE_LIMIT),
upstream_target_gate_limit: Some(DEFAULT_UPSTREAM_TARGET_GATE_LIMIT),
@@ -142,15 +154,152 @@ fn env_duration_ms(key: &str, default_ms: u64, min_ms: u64, max_ms: u64) -> Dura
Duration::from_millis(ms)
}
fn env_optional_usize(key: &str, default_value: usize) -> Option<usize> {
match std::env::var(key)
fn env_cache_duration_ms(key: &str, default_ms: u64, min_ms: u64, max_ms: u64) -> Duration {
let Some(raw) = std::env::var(key)
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
{
Some(0) => None,
Some(value) => Some(value.max(1)),
None => Some(default_value.max(1)),
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
else {
return Duration::from_millis(default_ms);
};
let Some(parsed) = raw.parse::<u64>().ok() else {
return Duration::from_millis(default_ms);
};
if parsed == 0 {
return Duration::ZERO;
}
Duration::from_millis(parsed.clamp(min_ms, max_ms))
}
#[derive(Debug, Clone, Copy)]
struct GateAutoProfile {
floor: usize,
cap: usize,
per_cpu: usize,
fd_divisor: Option<usize>,
}
#[derive(Debug, Clone, Copy)]
struct GateAutoCapacity {
cpu_parallelism: usize,
fd_soft_limit: usize,
}
const CANDIDATE_PLANNING_GATE_AUTO_PROFILE: GateAutoProfile = GateAutoProfile {
floor: DEFAULT_CANDIDATE_PLANNING_GATE_LIMIT,
cap: MAX_CANDIDATE_PLANNING_GATE_LIMIT,
per_cpu: CANDIDATE_PLANNING_GATE_LIMIT_PER_CPU,
fd_divisor: None,
};
const UPSTREAM_EXECUTION_GATE_AUTO_PROFILE: GateAutoProfile = GateAutoProfile {
floor: DEFAULT_UPSTREAM_EXECUTION_GATE_LIMIT,
cap: MAX_UPSTREAM_EXECUTION_GATE_LIMIT,
per_cpu: UPSTREAM_EXECUTION_GATE_LIMIT_PER_CPU,
fd_divisor: Some(2),
};
const UPSTREAM_TARGET_GATE_AUTO_PROFILE: GateAutoProfile = GateAutoProfile {
floor: DEFAULT_UPSTREAM_TARGET_GATE_LIMIT,
cap: MAX_UPSTREAM_TARGET_GATE_LIMIT,
per_cpu: UPSTREAM_TARGET_GATE_LIMIT_PER_CPU,
fd_divisor: Some(4),
};
fn candidate_planning_gate_limit_from_env() -> Option<usize> {
env_gate_limit(
CANDIDATE_PLANNING_GATE_LIMIT_ENV,
CANDIDATE_PLANNING_GATE_AUTO_PROFILE,
)
}
fn upstream_execution_gate_limit_from_env() -> Option<usize> {
env_gate_limit(
UPSTREAM_EXECUTION_GATE_LIMIT_ENV,
UPSTREAM_EXECUTION_GATE_AUTO_PROFILE,
)
}
pub(crate) fn upstream_target_gate_limit_from_env() -> Option<usize> {
env_gate_limit(
UPSTREAM_TARGET_GATE_LIMIT_ENV,
UPSTREAM_TARGET_GATE_AUTO_PROFILE,
)
}
pub(crate) fn upstream_target_gate_auto_limit() -> usize {
auto_gate_limit(
UPSTREAM_TARGET_GATE_AUTO_PROFILE,
current_gate_auto_capacity(),
)
}
fn env_gate_limit(key: &str, profile: GateAutoProfile) -> Option<usize> {
let raw = std::env::var(key).ok();
parse_gate_limit_value(raw.as_deref(), profile, current_gate_auto_capacity())
}
fn parse_gate_limit_value(
raw: Option<&str>,
profile: GateAutoProfile,
capacity: GateAutoCapacity,
) -> Option<usize> {
let Some(value) = raw.map(str::trim).filter(|value| !value.is_empty()) else {
return Some(auto_gate_limit(profile, capacity));
};
let normalized = value.to_ascii_lowercase();
match normalized.as_str() {
"auto" => Some(auto_gate_limit(profile, capacity)),
"off" | "none" | "disabled" | "disable" => None,
_ => match value.parse::<usize>() {
Ok(0) => None,
Ok(limit) => Some(limit.max(1)),
Err(_) => Some(auto_gate_limit(profile, capacity)),
},
}
}
fn auto_gate_limit(profile: GateAutoProfile, capacity: GateAutoCapacity) -> usize {
let cpu_limit = capacity
.cpu_parallelism
.max(1)
.saturating_mul(profile.per_cpu.max(1));
let mut limit = cpu_limit.max(profile.floor).min(profile.cap);
if let Some(fd_divisor) = profile.fd_divisor.filter(|value| *value > 0) {
let fd_budget = capacity
.fd_soft_limit
.saturating_sub(GATE_LIMIT_FD_RESERVE)
.checked_div(fd_divisor)
.unwrap_or(1)
.max(1);
limit = limit.min(fd_budget);
}
limit.max(1)
}
fn current_gate_auto_capacity() -> GateAutoCapacity {
GateAutoCapacity {
cpu_parallelism: std::thread::available_parallelism()
.map(|value| value.get())
.unwrap_or(1)
.max(1),
fd_soft_limit: soft_fd_limit().unwrap_or(1024).max(1),
}
}
fn soft_fd_limit() -> Option<usize> {
#[cfg(unix)]
{
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let result = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
if result == 0 {
return usize::try_from(limit.rlim_cur).ok();
}
}
None
}
#[derive(Debug, Clone)]
@@ -178,8 +327,15 @@ pub struct AppState {
pub(crate) auth_api_key_force_capabilities_cache:
Arc<JsonValueCache<AuthApiKeyIdentityCacheKey>>,
pub(crate) auth_api_key_feature_settings_cache: Arc<JsonValueCache<AuthApiKeyFeatureCacheKey>>,
pub(crate) auth_daily_quota_availability_cache:
Arc<ValueCache<String, UserDailyQuotaAvailabilityRecord>>,
pub(crate) auth_wallet_snapshot_cache:
Arc<ValueCache<String, aether_data::repository::wallet::StoredWalletSnapshot>>,
pub(crate) auth_request_cost_upper_bound_cache: Arc<ValueCache<String, f64>>,
pub(crate) provider_quota_snapshot_cache: Arc<ValueCache<String, StoredProviderQuotaSnapshot>>,
pub(crate) user_groups_for_user_cache: Arc<ValueCache<String, Vec<StoredUserGroup>>>,
pub(crate) routing_group_selection_cache:
Arc<ValueCache<String, crate::routing::GatewayRoutingGroupSelection>>,
pub(crate) auth_api_key_last_used_cache: Arc<AuthApiKeyLastUsedCache>,
pub(crate) oauth_refresh: Arc<provider_transport::LocalOAuthRefreshCoordinator>,
pub(crate) direct_plan_bypass_cache: Arc<DirectPlanBypassCache>,
@@ -187,6 +343,7 @@ pub struct AppState {
pub(crate) scheduler_affinity_epoch: Arc<AtomicU64>,
pub(crate) dashboard_response_cache: Arc<DashboardResponseCache>,
pub(crate) system_config_cache: Arc<SystemConfigCache>,
pub(crate) candidate_row_page_cache: Arc<super::super::cache::CandidateRowPageCache>,
pub(crate) candidate_page_cache: Arc<super::super::cache::CandidatePageCache>,
pub(crate) candidate_resolved_page_cache: Arc<super::super::cache::CandidateResolvedPageCache>,
pub(crate) chat_pii_redaction_runtime_config_cache:
@@ -197,10 +354,12 @@ pub struct AppState {
pub(crate) frontdoor_user_rpm: Arc<FrontdoorUserRpmLimiter>,
pub(crate) tunnel: crate::tunnel::EmbeddedTunnelState,
pub(crate) provider_transport_snapshot_cache:
Arc<StdRwLock<HashMap<ProviderTransportSnapshotCacheKey, CachedProviderTransportSnapshot>>>,
Arc<DashMap<ProviderTransportSnapshotCacheKey, CachedProviderTransportSnapshot>>,
pub(crate) provider_transport_snapshot_inflight:
Arc<DashMap<ProviderTransportSnapshotCacheKey, Arc<TokioMutex<()>>>>,
pub(crate) provider_key_rpm_resets: Arc<StdMutex<HashMap<String, u64>>>,
pub(crate) local_execution_runtime_miss_diagnostics:
Arc<StdMutex<HashMap<String, LocalExecutionRuntimeMissDiagnostic>>>,
Arc<DashMap<String, LocalExecutionRuntimeMissDiagnostic>>,
pub(crate) admin_monitoring_error_stats_reset_at: Arc<StdMutex<Option<u64>>>,
pub(crate) provider_delete_tasks: Arc<StdMutex<HashMap<String, LocalProviderDeleteTaskState>>>,
#[cfg(test)]
@@ -262,3 +421,69 @@ pub struct AppState {
#[cfg(test)]
pub(crate) provider_oauth_token_url_overrides: Arc<StdMutex<HashMap<String, String>>>,
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_PROFILE: GateAutoProfile = GateAutoProfile {
floor: 10_000,
cap: 16_384,
per_cpu: 1024,
fd_divisor: Some(2),
};
const TEST_CAPACITY: GateAutoCapacity = GateAutoCapacity {
cpu_parallelism: 12,
fd_soft_limit: 1_048_576,
};
#[test]
fn gate_limit_parser_defaults_to_auto() {
assert_eq!(
parse_gate_limit_value(None, TEST_PROFILE, TEST_CAPACITY),
Some(12_288)
);
assert_eq!(
parse_gate_limit_value(Some("auto"), TEST_PROFILE, TEST_CAPACITY),
Some(12_288)
);
assert_eq!(
parse_gate_limit_value(Some(" AUTO "), TEST_PROFILE, TEST_CAPACITY),
Some(12_288)
);
}
#[test]
fn gate_limit_parser_accepts_fixed_numbers() {
assert_eq!(
parse_gate_limit_value(Some("4096"), TEST_PROFILE, TEST_CAPACITY),
Some(4096)
);
}
#[test]
fn gate_limit_parser_accepts_off_and_legacy_zero() {
for value in ["off", "none", "disabled", "disable", "0"] {
assert_eq!(
parse_gate_limit_value(Some(value), TEST_PROFILE, TEST_CAPACITY),
None
);
}
}
#[test]
fn auto_gate_limit_respects_fd_budget_when_fd_limit_is_low() {
assert_eq!(
parse_gate_limit_value(
Some("auto"),
TEST_PROFILE,
GateAutoCapacity {
cpu_parallelism: 32,
fd_soft_limit: 1024,
},
),
Some(448)
);
}
}
+104 -2
View File
@@ -672,7 +672,30 @@ impl AppState {
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
if updated.is_some() {
self.invalidate_provider_health_routing_caches();
self.invalidate_provider_runtime_state_caches();
}
Ok(updated)
}
pub(crate) async fn update_provider_catalog_key_success_health_state(
&self,
key_id: &str,
is_active: bool,
health_by_format: Option<&serde_json::Value>,
circuit_breaker_by_format: Option<&serde_json::Value>,
) -> Result<bool, GatewayError> {
let updated = self
.data
.update_provider_catalog_key_health_state(
key_id,
is_active,
health_by_format,
circuit_breaker_by_format,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
if updated {
self.invalidate_provider_runtime_state_caches();
}
Ok(updated)
}
@@ -920,7 +943,8 @@ mod tests {
};
use async_trait::async_trait;
use crate::cache::SchedulerAffinityTarget;
use crate::cache::{CandidatePageCacheKey, SchedulerAffinityTarget};
use crate::data::auth::GatewayAuthApiKeySnapshot;
use crate::data::GatewayDataState;
use crate::AppState;
@@ -969,6 +993,35 @@ mod tests {
.expect("key should build")
}
fn sample_auth_snapshot() -> GatewayAuthApiKeySnapshot {
GatewayAuthApiKeySnapshot {
user_id: "user-1".to_string(),
username: "alice".to_string(),
email: None,
user_role: "user".to_string(),
user_auth_source: "local".to_string(),
user_is_active: true,
user_is_deleted: false,
user_rate_limit: None,
user_allowed_providers: None,
user_allowed_api_formats: None,
user_allowed_models: None,
api_key_id: "api-key-1".to_string(),
api_key_name: Some("default".to_string()),
api_key_is_active: true,
api_key_is_locked: false,
api_key_is_standalone: false,
api_key_rate_limit: None,
api_key_concurrent_limit: None,
api_key_expires_at_unix_secs: None,
api_key_allowed_providers: None,
api_key_allowed_api_formats: None,
api_key_allowed_models: None,
api_key_ip_rules: None,
currently_usable: true,
}
}
fn sample_admin_global_model() -> StoredAdminGlobalModel {
StoredAdminGlobalModel::new(
"global-1".to_string(),
@@ -1260,4 +1313,53 @@ mod tests {
Some(target)
);
}
#[tokio::test]
async fn provider_catalog_runtime_state_update_keeps_candidate_page_cache() {
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider()],
vec![sample_endpoint()],
vec![sample_key()],
));
let state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(
GatewayDataState::with_provider_catalog_repository_for_tests(repository)
.with_encryption_key_for_tests("test-encryption-key"),
);
let ttl = Duration::from_secs(300);
let cache_key = CandidatePageCacheKey::new(
"gpt-5",
"openai:chat",
true,
&sample_auth_snapshot(),
None,
None,
None,
state.scheduler_affinity_epoch(),
"fixed_order",
true,
None,
);
state.candidate_page_cache.insert(
cache_key.clone(),
Some(Arc::new(crate::cache::CandidatePageSnapshot {
candidates: Vec::new(),
skipped_candidates: Vec::new(),
})),
ttl,
);
assert!(state.candidate_page_cache.get(&cache_key, ttl).is_some());
let mut updated_key = sample_key();
updated_key.status_snapshot = Some(serde_json::json!({"source": "runtime"}));
let updated = state
.update_provider_catalog_key_runtime_state(&updated_key)
.await
.expect("runtime state update should succeed");
assert!(updated.is_some());
assert!(state.candidate_page_cache.get(&cache_key, ttl).is_some());
}
}
+145 -20
View File
@@ -20,6 +20,7 @@ use aether_runtime_state::{
RuntimeSemaphoreSnapshot, RuntimeState,
};
use aether_scheduler_core::PROVIDER_KEY_RPM_WINDOW_SECS;
use dashmap::DashMap;
use tracing::warn;
use super::{
@@ -100,6 +101,10 @@ fn system_config_key_affects_chat_pii_redaction(key: &str) -> bool {
.starts_with(CHAT_PII_REDACTION_SYSTEM_CONFIG_PREFIX)
}
fn system_config_key_affects_provider_transport_snapshot(key: &str) -> bool {
key.trim() == "enable_format_conversion"
}
impl AppState {
pub async fn prewarm_chat_pii_redaction_runtime_config(&self) -> Result<bool, String> {
crate::privacy::read_chat_pii_redaction_runtime_config(self)
@@ -185,6 +190,7 @@ impl AppState {
self.clear_provider_transport_snapshot_cache();
self.invalidate_scheduler_affinity_cache();
self.invalidate_auth_context_cache();
self.candidate_row_page_cache.clear();
self.candidate_resolved_page_cache.clear();
self.system_config_cache.clear();
self.frontdoor_user_rpm.clear_system_default_cache();
@@ -193,6 +199,7 @@ impl AppState {
.clone()
.with_usage_worker_queue(Self::usage_worker_queue_for(&self.runtime_state)),
);
self.candidate_row_page_cache.clear();
self.candidate_page_cache.clear();
self.candidate_resolved_page_cache.clear();
self.tunnel = crate::tunnel::EmbeddedTunnelState::with_data_and_runtime_state(
@@ -275,8 +282,12 @@ impl AppState {
user_feature_settings_cache: Arc::new(JsonValueCache::default()),
auth_api_key_force_capabilities_cache: Arc::new(JsonValueCache::default()),
auth_api_key_feature_settings_cache: Arc::new(JsonValueCache::default()),
auth_daily_quota_availability_cache: Arc::new(ValueCache::default()),
auth_wallet_snapshot_cache: Arc::new(ValueCache::default()),
auth_request_cost_upper_bound_cache: Arc::new(ValueCache::default()),
provider_quota_snapshot_cache: Arc::new(ValueCache::default()),
user_groups_for_user_cache: Arc::new(ValueCache::default()),
routing_group_selection_cache: Arc::new(ValueCache::default()),
auth_api_key_last_used_cache: Arc::new(AuthApiKeyLastUsedCache::default()),
oauth_refresh: Arc::new(provider_transport::LocalOAuthRefreshCoordinator::new()),
direct_plan_bypass_cache: Arc::new(DirectPlanBypassCache::default()),
@@ -284,6 +295,7 @@ impl AppState {
scheduler_affinity_epoch: Arc::new(AtomicU64::new(0)),
dashboard_response_cache: Arc::new(DashboardResponseCache::default()),
system_config_cache: Arc::new(SystemConfigCache::default()),
candidate_row_page_cache: Arc::new(crate::cache::CandidateRowPageCache::default()),
candidate_page_cache: Arc::new(crate::cache::CandidatePageCache::default()),
candidate_resolved_page_cache: Arc::new(
crate::cache::CandidateResolvedPageCache::default(),
@@ -300,9 +312,10 @@ impl AppState {
data,
runtime_state.clone(),
),
provider_transport_snapshot_cache: Arc::new(std::sync::RwLock::new(HashMap::new())),
provider_transport_snapshot_cache: Arc::new(DashMap::new()),
provider_transport_snapshot_inflight: Arc::new(DashMap::new()),
provider_key_rpm_resets: Arc::new(StdMutex::new(HashMap::new())),
local_execution_runtime_miss_diagnostics: Arc::new(StdMutex::new(HashMap::new())),
local_execution_runtime_miss_diagnostics: Arc::new(DashMap::new()),
admin_monitoring_error_stats_reset_at: Arc::new(StdMutex::new(None)),
provider_delete_tasks: Arc::new(StdMutex::new(HashMap::new())),
#[cfg(test)]
@@ -671,12 +684,18 @@ impl AppState {
&self.chat_pii_redaction_runtime_config_cache,
);
}
if deleted && system_config_key_affects_provider_transport_snapshot(key) {
self.clear_provider_transport_snapshot_cache();
}
Ok(deleted)
}
pub(crate) fn invalidate_provider_routing_caches(&self) {
self.data.clear_minimal_candidate_selection_cache();
self.data.clear_routing_group_cache();
self.data.clear_provider_catalog_cache();
self.routing_group_selection_cache.clear();
self.candidate_row_page_cache.clear();
self.candidate_page_cache.clear();
self.candidate_resolved_page_cache.clear();
self.clear_provider_transport_snapshot_cache();
@@ -686,11 +705,18 @@ impl AppState {
pub(crate) fn invalidate_provider_health_routing_caches(&self) {
self.data.clear_minimal_candidate_selection_cache();
self.data.clear_provider_catalog_cache();
self.candidate_row_page_cache.clear();
self.candidate_page_cache.clear();
self.candidate_resolved_page_cache.clear();
self.clear_provider_transport_snapshot_cache();
}
pub(crate) fn invalidate_provider_runtime_state_caches(&self) {
self.data.clear_minimal_candidate_selection_cache();
self.data.clear_provider_catalog_cache();
self.clear_provider_transport_snapshot_cache();
}
pub(crate) fn invalidate_auth_context_cache(&self) {
self.auth_context_cache.clear();
self.auth_snapshot_cache.clear();
@@ -698,8 +724,13 @@ impl AppState {
self.user_feature_settings_cache.clear();
self.auth_api_key_force_capabilities_cache.clear();
self.auth_api_key_feature_settings_cache.clear();
self.auth_daily_quota_availability_cache.clear();
self.auth_wallet_snapshot_cache.clear();
self.auth_request_cost_upper_bound_cache.clear();
self.provider_quota_snapshot_cache.clear();
self.user_groups_for_user_cache.clear();
self.routing_group_selection_cache.clear();
self.candidate_row_page_cache.clear();
self.candidate_page_cache.clear();
self.candidate_resolved_page_cache.clear();
}
@@ -721,6 +752,9 @@ impl AppState {
&self.chat_pii_redaction_runtime_config_cache,
);
}
if system_config_key_affects_provider_transport_snapshot(key) {
self.clear_provider_transport_snapshot_cache();
}
}
pub(crate) async fn read_admin_system_stats(
@@ -1041,6 +1075,9 @@ impl AppState {
if let Some(queue) = self.request_candidate_queue.as_ref() {
samples.extend(queue.metric_samples());
}
samples.extend(usage_runtime_metric_samples(
&self.usage_runtime.metrics_snapshot(),
));
samples.extend(
crate::execution_runtime::transport::direct_reqwest_client_cache_metric_samples(),
);
@@ -1066,8 +1103,6 @@ impl AppState {
pub(crate) fn clear_local_execution_runtime_miss_diagnostic(&self, trace_id: &str) {
self.local_execution_runtime_miss_diagnostics
.lock()
.expect("local execution runtime miss diagnostics should lock")
.remove(trace_id);
}
@@ -1076,17 +1111,17 @@ impl AppState {
trace_id: &str,
diagnostic: LocalExecutionRuntimeMissDiagnostic,
) {
let mut diagnostics = self
if self
.local_execution_runtime_miss_diagnostics
.lock()
.expect("local execution runtime miss diagnostics should lock");
if diagnostics
.get(trace_id)
.is_some_and(|existing| should_preserve_runtime_miss_diagnostic(existing, &diagnostic))
.is_some_and(|existing| {
should_preserve_runtime_miss_diagnostic(existing.value(), &diagnostic)
})
{
return;
}
diagnostics.insert(trace_id.to_string(), diagnostic);
self.local_execution_runtime_miss_diagnostics
.insert(trace_id.to_string(), diagnostic);
}
pub(crate) fn mutate_local_execution_runtime_miss_diagnostic<F>(
@@ -1096,12 +1131,11 @@ impl AppState {
) where
F: FnOnce(&mut LocalExecutionRuntimeMissDiagnostic),
{
let mut diagnostics = self
if let Some(mut diagnostic) = self
.local_execution_runtime_miss_diagnostics
.lock()
.expect("local execution runtime miss diagnostics should lock");
if let Some(diagnostic) = diagnostics.get_mut(trace_id) {
mutate(diagnostic);
.get_mut(trace_id)
{
mutate(&mut diagnostic);
}
}
@@ -1110,10 +1144,10 @@ impl AppState {
trace_id: &str,
) -> bool {
self.local_execution_runtime_miss_diagnostics
.lock()
.expect("local execution runtime miss diagnostics should lock")
.get(trace_id)
.is_some_and(runtime_miss_diagnostic_has_candidate_signal)
.is_some_and(|diagnostic| {
runtime_miss_diagnostic_has_candidate_signal(diagnostic.value())
})
}
pub(crate) fn take_local_execution_runtime_miss_diagnostic(
@@ -1121,9 +1155,8 @@ impl AppState {
trace_id: &str,
) -> Option<LocalExecutionRuntimeMissDiagnostic> {
self.local_execution_runtime_miss_diagnostics
.lock()
.expect("local execution runtime miss diagnostics should lock")
.remove(trace_id)
.map(|(_, diagnostic)| diagnostic)
}
pub(crate) async fn try_acquire_request_permit(
@@ -1231,6 +1264,7 @@ impl AppState {
.fetch_add(1, Ordering::AcqRel)
.saturating_add(1);
self.scheduler_affinity_cache.clear();
self.candidate_row_page_cache.clear();
self.candidate_page_cache.clear();
self.candidate_resolved_page_cache.clear();
next_epoch
@@ -1495,6 +1529,97 @@ fn database_pool_metric_samples(summary: &aether_data::DatabasePoolSummary) -> V
]
}
fn usage_runtime_metric_samples(
snapshot: &usage::UsageRuntimeMetricsSnapshot,
) -> Vec<MetricSample> {
vec![
MetricSample::new(
"usage_runtime_enabled",
"Whether the gateway usage runtime is enabled.",
MetricKind::Gauge,
u64::from(snapshot.enabled),
),
MetricSample::new(
"usage_runtime_queue_terminal_events_enabled",
"Whether terminal usage events are queued before settlement.",
MetricKind::Gauge,
u64::from(snapshot.queue_terminal_events),
),
MetricSample::new(
"usage_runtime_queue_lifecycle_events_enabled",
"Whether lifecycle usage events are queued.",
MetricKind::Gauge,
u64::from(snapshot.queue_lifecycle_events),
),
MetricSample::new(
"usage_runtime_retry_deferred_lifecycle_events_enabled",
"Whether deferred lifecycle usage events are scheduled for local enqueue retry.",
MetricKind::Gauge,
u64::from(snapshot.retry_deferred_lifecycle_events),
),
MetricSample::new(
"usage_runtime_terminal_enqueue_in_flight",
"Current terminal usage enqueue operations in flight.",
MetricKind::Gauge,
snapshot.terminal_enqueue_in_flight,
),
MetricSample::new(
"usage_runtime_terminal_enqueue_deferred_total",
"Total terminal usage enqueue operations deferred by circuit or in-flight limits.",
MetricKind::Counter,
snapshot.terminal_enqueue_deferred_total,
),
MetricSample::new(
"usage_runtime_terminal_enqueue_deferred_retry_total",
"Total deferred terminal usage events scheduled for local retry.",
MetricKind::Counter,
snapshot.terminal_enqueue_deferred_retry_total,
),
MetricSample::new(
"usage_runtime_terminal_enqueue_failed_total",
"Total terminal usage enqueue failures that opened the terminal enqueue circuit.",
MetricKind::Counter,
snapshot.terminal_enqueue_failed_total,
),
MetricSample::new(
"usage_runtime_lifecycle_enqueue_in_flight",
"Current lifecycle usage enqueue operations in flight.",
MetricKind::Gauge,
snapshot.lifecycle_enqueue_in_flight,
),
MetricSample::new(
"usage_runtime_lifecycle_enqueue_deferred_total",
"Total lifecycle usage enqueue operations deferred by circuit or in-flight limits.",
MetricKind::Counter,
snapshot.lifecycle_enqueue_deferred_total,
),
MetricSample::new(
"usage_runtime_lifecycle_enqueue_deferred_dropped_total",
"Total deferred lifecycle usage events dropped instead of retrying.",
MetricKind::Counter,
snapshot.lifecycle_enqueue_deferred_dropped_total,
),
MetricSample::new(
"usage_runtime_lifecycle_enqueue_deferred_retry_total",
"Total deferred lifecycle usage events scheduled for local retry.",
MetricKind::Counter,
snapshot.lifecycle_enqueue_deferred_retry_total,
),
MetricSample::new(
"usage_runtime_lifecycle_enqueue_failed_total",
"Total lifecycle usage enqueue failures that opened the lifecycle enqueue circuit.",
MetricKind::Counter,
snapshot.lifecycle_enqueue_failed_total,
),
MetricSample::new(
"usage_runtime_enqueue_retry_scheduled_total",
"Total usage events scheduled into the local enqueue retry dispatcher.",
MetricKind::Counter,
snapshot.enqueue_retry_scheduled_total,
),
]
}
fn should_preserve_runtime_miss_diagnostic(
existing: &LocalExecutionRuntimeMissDiagnostic,
next: &LocalExecutionRuntimeMissDiagnostic,
+4 -1
View File
@@ -27,7 +27,10 @@ pub(crate) use self::admin_types::{
UserDailyQuotaAvailabilityRecord, UserPlanEntitlementRecord,
};
pub use self::app::AppState;
pub(crate) use self::app::FrontdoorRuntimeGuardConfig;
pub(crate) use self::app::{
upstream_target_gate_auto_limit, upstream_target_gate_limit_from_env,
FrontdoorRuntimeGuardConfig,
};
pub(crate) use self::cache::{
CachedProviderTransportSnapshot, AUTH_API_KEY_LAST_USED_MAX_ENTRIES,
AUTH_API_KEY_LAST_USED_TTL, PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES,
+64 -37
View File
@@ -479,34 +479,29 @@ impl<'a> provider_transport::LocalOAuthHttpExecutor for GatewayLocalOAuthHttpExe
impl AppState {
pub(crate) fn clear_provider_transport_snapshot_cache(&self) {
self.provider_transport_snapshot_cache
.write()
.expect("provider transport snapshot cache should lock")
.clear();
self.provider_transport_snapshot_cache.clear();
self.provider_transport_snapshot_inflight.clear();
}
fn get_cached_provider_transport_snapshot_arc(
&self,
cache_key: &ProviderTransportSnapshotCacheKey,
) -> Option<Arc<provider_transport::GatewayProviderTransportSnapshot>> {
let cached = {
let cache = self
.provider_transport_snapshot_cache
.read()
.expect("provider transport snapshot cache should lock");
cache.get(cache_key).cloned()
}?;
let cached = self
.provider_transport_snapshot_cache
.get(cache_key)
.map(|entry| entry.clone())?;
if cached.loaded_at.elapsed() <= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_STALE_TTL {
return Some(cached.snapshot);
}
let mut cache = self
if self
.provider_transport_snapshot_cache
.write()
.expect("provider transport snapshot cache should lock");
if cache.get(cache_key).is_some_and(|entry| {
entry.loaded_at.elapsed() > PROVIDER_TRANSPORT_SNAPSHOT_CACHE_STALE_TTL
}) {
cache.remove(cache_key);
.get(cache_key)
.is_some_and(|entry| {
entry.loaded_at.elapsed() > PROVIDER_TRANSPORT_SNAPSHOT_CACHE_STALE_TTL
})
{
self.provider_transport_snapshot_cache.remove(cache_key);
}
None
}
@@ -516,19 +511,19 @@ impl AppState {
cache_key: ProviderTransportSnapshotCacheKey,
snapshot: Arc<provider_transport::GatewayProviderTransportSnapshot>,
) {
let mut cache = self
.provider_transport_snapshot_cache
.write()
.expect("provider transport snapshot cache should lock");
if cache.len() >= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES {
cache.retain(|_, entry| {
if self.provider_transport_snapshot_cache.len()
>= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES
{
self.provider_transport_snapshot_cache.retain(|_, entry| {
entry.loaded_at.elapsed() <= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_STALE_TTL
});
if cache.len() >= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES {
cache.clear();
if self.provider_transport_snapshot_cache.len()
>= PROVIDER_TRANSPORT_SNAPSHOT_CACHE_MAX_ENTRIES
{
self.provider_transport_snapshot_cache.clear();
}
}
cache.insert(
self.provider_transport_snapshot_cache.insert(
cache_key,
CachedProviderTransportSnapshot {
loaded_at: std::time::Instant::now(),
@@ -828,18 +823,37 @@ impl AppState {
return Ok(Some(snapshot));
}
let snapshot = self
.read_provider_transport_snapshot_uncached(provider_id, endpoint_id, key_id)
.await?;
match snapshot {
Some(snapshot) => {
let snapshot = self.apply_global_format_conversion_override(snapshot).await;
let snapshot = Arc::new(snapshot);
self.put_cached_provider_transport_snapshot(cache_key, Arc::clone(&snapshot));
let inflight = self
.provider_transport_snapshot_inflight
.entry(cache_key.clone())
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(())))
.clone();
let result = {
let _inflight_guard = inflight.lock().await;
if let Some(snapshot) = self.get_cached_provider_transport_snapshot_arc(&cache_key) {
Ok(Some(snapshot))
} else {
match self
.read_provider_transport_snapshot_uncached(provider_id, endpoint_id, key_id)
.await
{
Ok(Some(snapshot)) => {
let snapshot = self.apply_global_format_conversion_override(snapshot).await;
let snapshot = Arc::new(snapshot);
self.put_cached_provider_transport_snapshot(
cache_key.clone(),
Arc::clone(&snapshot),
);
Ok(Some(snapshot))
}
Ok(None) => Ok(None),
Err(err) => Err(err),
}
}
None => Ok(None),
}
};
self.provider_transport_snapshot_inflight
.remove_if(&cache_key, |_, current| Arc::ptr_eq(current, &inflight));
result
}
pub(crate) async fn read_provider_transport_snapshot(
@@ -1768,6 +1782,19 @@ mod tests {
assert!(!snapshot.provider.enable_format_conversion);
}
#[tokio::test]
async fn provider_transport_snapshot_inflight_entry_is_removed_after_read() {
let state = state_with_global_format_conversion(false);
let snapshot = state
.read_provider_transport_snapshot_arc("provider-1", "endpoint-1", "key-1")
.await
.expect("snapshot read should succeed");
assert!(snapshot.is_some());
assert!(state.provider_transport_snapshot_inflight.is_empty());
}
#[test]
fn normalizes_local_openai_refresh_token_expired_response() {
let body = r#"{"error":{"message":"Could not validate your refresh token. Please try signing in again.","type":"invalid_request_error","param":null,"code":"refresh_token_expired"}}"#;
@@ -514,4 +514,23 @@ impl AppState {
.await
.map_err(data_error)
}
pub(crate) async fn find_user_daily_quota_availability_for_auth(
&self,
user_id: &str,
) -> Result<Option<UserDailyQuotaAvailabilityRecord>, GatewayError> {
let user_id = user_id.trim();
if user_id.is_empty() {
return Ok(None);
}
let ttl = self.frontdoor_runtime_guards.auth_capacity_cache_ttl;
if ttl.is_zero() {
return self.find_user_daily_quota_availability(user_id).await;
}
self.auth_daily_quota_availability_cache
.get_or_load(user_id.to_string(), ttl, || async move {
self.find_user_daily_quota_availability(user_id).await
})
.await
}
}
@@ -42,19 +42,70 @@ impl AppState {
api_key_id: &str,
api_key_is_standalone: bool,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
let user_id = user_id.trim();
let api_key_id = api_key_id.trim();
let lookup = if api_key_is_standalone {
if api_key_id.trim().is_empty() {
if api_key_id.is_empty() {
None
} else {
Some((
format!("api_key:{api_key_id}"),
aether_data::repository::wallet::WalletLookupKey::ApiKeyId(api_key_id),
))
}
} else if !user_id.is_empty() {
Some((
format!("user:{user_id}"),
aether_data::repository::wallet::WalletLookupKey::UserId(user_id),
))
} else if !api_key_id.is_empty() {
Some((
format!("api_key:{api_key_id}"),
aether_data::repository::wallet::WalletLookupKey::ApiKeyId(api_key_id),
))
} else {
None
};
let Some((cache_key, lookup)) = lookup else {
return Ok(None);
};
let ttl = self.frontdoor_runtime_guards.auth_capacity_cache_ttl;
if ttl.is_zero() {
return self.find_wallet(lookup).await;
}
self.auth_wallet_snapshot_cache
.get_or_load(
cache_key,
ttl,
|| async move { self.find_wallet(lookup).await },
)
.await
}
pub(crate) async fn read_wallet_snapshot_for_auth_uncached(
&self,
user_id: &str,
api_key_id: &str,
api_key_is_standalone: bool,
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
let user_id = user_id.trim();
let api_key_id = api_key_id.trim();
let lookup = if api_key_is_standalone {
if api_key_id.is_empty() {
None
} else {
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
api_key_id,
))
}
} else if !user_id.trim().is_empty() {
} else if !user_id.is_empty() {
Some(aether_data::repository::wallet::WalletLookupKey::UserId(
user_id,
))
} else if !api_key_id.trim().is_empty() {
} else if !api_key_id.is_empty() {
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
api_key_id,
))
+1
View File
@@ -18,6 +18,7 @@ use crate::{provider_transport, usage};
impl AppState {
pub(crate) fn with_data_state_for_tests(mut self, data_state: GatewayDataState) -> Self {
self.replace_data_state(Arc::new(data_state));
self.request_candidate_queue = None;
self
}
+268
View File
@@ -0,0 +1,268 @@
use std::sync::Arc;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
};
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data::repository::usage::InMemoryUsageReadRepository;
use aether_data_contracts::repository::candidate_selection::{
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
};
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
};
use sha2::{Digest, Sha256};
use crate::data::GatewayDataState;
use crate::AppState;
#[derive(Debug, Clone)]
pub struct OpenAiChatPressureTarget {
pub base_url: String,
}
#[derive(Debug, Clone)]
pub struct OpenAiChatPressureStateConfig {
pub client_api_key: String,
pub api_key_id: String,
pub user_id: String,
pub requested_model: String,
pub provider_model: String,
pub targets: Vec<OpenAiChatPressureTarget>,
pub max_in_flight_requests: Option<usize>,
}
impl OpenAiChatPressureStateConfig {
pub fn new(target_base_urls: Vec<String>) -> Self {
Self {
client_api_key: "sk-aether-openai-chat-pressure".to_string(),
api_key_id: "api-key-openai-chat-pressure".to_string(),
user_id: "user-openai-chat-pressure".to_string(),
requested_model: "gpt-5".to_string(),
provider_model: "gpt-5-upstream".to_string(),
targets: target_base_urls
.into_iter()
.map(|base_url| OpenAiChatPressureTarget { base_url })
.collect(),
max_in_flight_requests: None,
}
}
}
pub fn build_openai_chat_pressure_state(
config: OpenAiChatPressureStateConfig,
) -> Result<AppState, String> {
if config.targets.is_empty() {
return Err("at least one pressure target is required".to_string());
}
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
Some(hash_api_key(&config.client_api_key)),
openai_chat_pressure_auth_snapshot(&config),
)]));
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(
openai_chat_pressure_candidates(&config),
));
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![openai_chat_pressure_provider()],
openai_chat_pressure_endpoints(&config),
openai_chat_pressure_keys(&config)?,
));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let usage_repository = Arc::new(InMemoryUsageReadRepository::default());
let data_state = GatewayDataState::with_openai_chat_pressure_repositories_for_testkit(
auth_repository,
candidate_selection_repository,
provider_catalog_repository,
request_candidate_repository,
usage_repository,
DEVELOPMENT_ENCRYPTION_KEY,
);
let mut state =
AppState::new().map_err(|err| format!("failed to build pressure gateway state: {err}"))?;
state.replace_data_state(Arc::new(data_state));
if let Some(limit) = config.max_in_flight_requests {
state = state.with_request_concurrency_limit(limit);
}
Ok(state)
}
fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
format!("{:x}", hasher.finalize())
}
fn openai_chat_pressure_auth_snapshot(
config: &OpenAiChatPressureStateConfig,
) -> StoredAuthApiKeySnapshot {
StoredAuthApiKeySnapshot::new(
config.user_id.clone(),
"pressure".to_string(),
Some("pressure@example.com".to_string()),
"user".to_string(),
"local".to_string(),
true,
false,
Some(serde_json::json!(["openai"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!([config.requested_model.clone()])),
config.api_key_id.clone(),
Some("pressure".to_string()),
true,
false,
false,
Some(600_000),
Some(20_000),
Some(4_102_444_800),
Some(serde_json::json!(["openai"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!([config.requested_model.clone()])),
)
.expect("pressure auth snapshot should build")
}
fn openai_chat_pressure_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-chat-pressure".to_string(),
"openai".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
)
.expect("pressure provider should build")
.with_transport_fields(true, false, false, None, None, None, Some(20.0), None, None)
}
fn openai_chat_pressure_endpoints(
config: &OpenAiChatPressureStateConfig,
) -> Vec<StoredProviderCatalogEndpoint> {
config
.targets
.iter()
.enumerate()
.map(|(index, target)| {
StoredProviderCatalogEndpoint::new(
pressure_endpoint_id(index),
"provider-openai-chat-pressure".to_string(),
"openai:chat".to_string(),
Some("openai".to_string()),
Some("chat".to_string()),
true,
)
.expect("pressure endpoint should build")
.with_transport_fields(
target.base_url.trim_end_matches('/').to_string(),
None,
None,
None,
None,
None,
None,
None,
)
.expect("pressure endpoint transport should build")
})
.collect()
}
fn openai_chat_pressure_keys(
config: &OpenAiChatPressureStateConfig,
) -> Result<Vec<StoredProviderCatalogKey>, String> {
config
.targets
.iter()
.enumerate()
.map(|(index, _)| {
let encrypted = encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
&format!("sk-upstream-openai-chat-pressure-{index}"),
)
.map_err(|err| format!("failed to encrypt pressure key: {err}"))?;
StoredProviderCatalogKey::new(
pressure_key_id(index),
"provider-openai-chat-pressure".to_string(),
format!("pressure-{index}"),
"api_key".to_string(),
None,
true,
)
.expect("pressure key should build")
.with_transport_fields(
Some(serde_json::json!(["openai:chat"])),
encrypted,
None,
None,
Some(serde_json::json!({"openai:chat": 1})),
None,
None,
None,
None,
)
.map_err(|err| format!("failed to build pressure key transport: {err}"))
})
.collect()
}
fn openai_chat_pressure_candidates(
config: &OpenAiChatPressureStateConfig,
) -> Vec<StoredMinimalCandidateSelectionRow> {
config
.targets
.iter()
.enumerate()
.map(|(index, _)| StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-chat-pressure".to_string(),
provider_name: "openai".to_string(),
provider_type: "custom".to_string(),
provider_priority: 10,
provider_is_active: true,
endpoint_id: pressure_endpoint_id(index),
endpoint_api_format: "openai:chat".to_string(),
endpoint_api_family: Some("openai".to_string()),
endpoint_kind: Some("chat".to_string()),
endpoint_is_active: true,
key_id: pressure_key_id(index),
key_name: format!("pressure-{index}"),
key_auth_type: "api_key".to_string(),
key_is_active: true,
key_api_formats: Some(vec!["openai:chat".to_string()]),
key_allowed_models: None,
key_capabilities: None,
key_internal_priority: 5,
key_global_priority_by_format: Some(serde_json::json!({"openai:chat": 1})),
model_id: pressure_model_id(index),
global_model_id: "global-model-openai-chat-pressure".to_string(),
global_model_name: config.requested_model.clone(),
global_model_mappings: None,
global_model_supports_streaming: Some(true),
model_provider_model_name: config.provider_model.clone(),
model_provider_model_mappings: Some(vec![StoredProviderModelMapping {
name: config.provider_model.clone(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec![pressure_endpoint_id(index)]),
}]),
model_supports_streaming: Some(true),
model_is_active: true,
model_is_available: true,
})
.collect()
}
fn pressure_endpoint_id(index: usize) -> String {
format!("endpoint-openai-chat-pressure-{index}")
}
fn pressure_key_id(index: usize) -> String {
format!("key-openai-chat-pressure-{index}")
}
fn pressure_model_id(index: usize) -> String {
format!("model-openai-chat-pressure-{index}")
}
@@ -1468,8 +1468,15 @@ async fn gateway_executes_openai_chat_cross_format_tool_use_upstream_stream_via_
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported(
#[test]
fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported() {
run_openai_chat_finalize_test(
"gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported",
gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported_impl,
);
}
async fn gateway_skips_openai_chat_antigravity_cross_format_sync_candidate_as_transport_unsupported_impl(
) {
fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new();
@@ -1,9 +1,9 @@
use super::{
any, build_router_with_execution_runtime_override, build_router_with_state,
build_state_with_execution_runtime_override, json, start_server, to_bytes, Arc, Body, Bytes,
HeaderName, HeaderValue, Json, Mutex, Request, Response, Router, StatusCode,
CONTROL_EXECUTED_HEADER, EXECUTION_PATH_EXECUTION_RUNTIME_SYNC, EXECUTION_PATH_HEADER,
TRACE_ID_HEADER,
build_state_with_execution_runtime_override, json, run_finalize_local_cli_test, start_server,
to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue, Json, Mutex, Request, Response, Router,
StatusCode, CONTROL_EXECUTED_HEADER, EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
};
use crate::data::GatewayDataState;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
@@ -25,8 +25,16 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream_via_local_finalize_response(
#[test]
fn gateway_executes_openai_responses_compact_openai_family_upstream_stream_via_local_finalize_response(
) {
run_finalize_local_cli_test(
"gateway_executes_openai_responses_compact_openai_family_upstream_stream_via_local_finalize_response",
gateway_executes_openai_responses_compact_openai_family_upstream_stream_via_local_finalize_response_impl,
);
}
async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream_via_local_finalize_response_impl(
) {
use base64::Engine as _;
@@ -1,9 +1,9 @@
use super::{
any, build_router_with_execution_runtime_override, build_router_with_state,
build_state_with_execution_runtime_override, json, start_server, to_bytes, Arc, Body, Bytes,
HeaderName, HeaderValue, Json, Mutex, Request, Response, Router, StatusCode,
CONTROL_EXECUTED_HEADER, EXECUTION_PATH_EXECUTION_RUNTIME_SYNC, EXECUTION_PATH_HEADER,
TRACE_ID_HEADER,
build_state_with_execution_runtime_override, json, run_finalize_local_cli_test, start_server,
to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue, Json, Mutex, Request, Response, Router,
StatusCode, CONTROL_EXECUTED_HEADER, EXECUTION_PATH_EXECUTION_RUNTIME_SYNC,
EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
};
use crate::data::GatewayDataState;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
@@ -25,8 +25,15 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_local_finalize_response(
#[test]
fn gateway_executes_openai_responses_cross_format_upstream_stream_via_local_finalize_response() {
run_finalize_local_cli_test(
"gateway_executes_openai_responses_cross_format_upstream_stream_via_local_finalize_response",
gateway_executes_openai_responses_cross_format_upstream_stream_via_local_finalize_response_impl,
);
}
async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_local_finalize_response_impl(
) {
use base64::Engine as _;
@@ -490,8 +497,16 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_openai_responses_cross_format_function_call_upstream_stream_via_local_finalize_response(
#[test]
fn gateway_executes_openai_responses_cross_format_function_call_upstream_stream_via_local_finalize_response(
) {
run_finalize_local_cli_test(
"gateway_executes_openai_responses_cross_format_function_call_upstream_stream_via_local_finalize_response",
gateway_executes_openai_responses_cross_format_function_call_upstream_stream_via_local_finalize_response_impl,
);
}
async fn gateway_executes_openai_responses_cross_format_function_call_upstream_stream_via_local_finalize_response_impl(
) {
use base64::Engine as _;
@@ -953,8 +968,16 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_stream_via_local_finalize_response(
#[test]
fn gateway_executes_openai_responses_antigravity_cross_format_upstream_stream_via_local_finalize_response(
) {
run_finalize_local_cli_test(
"gateway_executes_openai_responses_antigravity_cross_format_upstream_stream_via_local_finalize_response",
gateway_executes_openai_responses_antigravity_cross_format_upstream_stream_via_local_finalize_response_impl,
);
}
async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_stream_via_local_finalize_response_impl(
) {
use base64::Engine as _;
@@ -49,8 +49,15 @@ where
}
}
#[tokio::test]
async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finalize_response() {
#[test]
fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finalize_response() {
run_kiro_claude_cli_finalize_test(
"gateway_executes_openai_responses_sync_upstream_stream_via_local_finalize_response",
gateway_executes_openai_responses_sync_upstream_stream_via_local_finalize_response_impl,
);
}
async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finalize_response_impl() {
use base64::Engine as _;
#[derive(Debug, Clone)]
@@ -23,6 +23,30 @@ use super::{
UsageRuntimeConfig, VideoTaskTruthSourceMode,
};
const FINALIZE_LOCAL_CLI_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_finalize_local_cli_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(FINALIZE_LOCAL_CLI_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("finalize local cli test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
mod compact;
mod cross_format;
mod direct;
@@ -22,6 +22,30 @@ use sha2::{Digest, Sha256};
use crate::data::GatewayDataState;
use crate::tests::next_non_keepalive_chunk;
const LIFECYCLE_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_lifecycle_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(LIFECYCLE_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("lifecycle test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
@@ -163,8 +187,15 @@ fn sample_local_openai_key() -> StoredProviderCatalogKey {
.expect("key transport should build")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn gateway_completes_sync_response_on_local_execution_runtime_path() {
#[test]
fn gateway_completes_sync_response_on_local_execution_runtime_path() {
run_lifecycle_test(
"gateway_completes_sync_response_on_local_execution_runtime_path",
gateway_completes_sync_response_on_local_execution_runtime_path_impl,
);
}
async fn gateway_completes_sync_response_on_local_execution_runtime_path_impl() {
let public_hits = Arc::new(Mutex::new(0usize));
let public_hits_clone = Arc::clone(&public_hits);
let upstream = Router::new().route(
@@ -261,8 +292,15 @@ async fn gateway_completes_sync_response_on_local_execution_runtime_path() {
upstream_handle.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn gateway_stops_execution_runtime_stream_when_client_disconnects() {
#[test]
fn gateway_stops_execution_runtime_stream_when_client_disconnects() {
run_lifecycle_test(
"gateway_stops_execution_runtime_stream_when_client_disconnects",
gateway_stops_execution_runtime_stream_when_client_disconnects_impl,
);
}
async fn gateway_stops_execution_runtime_stream_when_client_disconnects_impl() {
let seen_report = Arc::new(Mutex::new(0usize));
let seen_report_clone = Arc::clone(&seen_report);
let public_hits = Arc::new(Mutex::new(0usize));
@@ -388,8 +426,15 @@ async fn gateway_stops_execution_runtime_stream_when_client_disconnects() {
upstream_handle.abort();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn gateway_returns_error_body_when_prefetch_detects_embedded_stream_error() {
#[test]
fn gateway_returns_error_body_when_prefetch_detects_embedded_stream_error() {
run_lifecycle_test(
"gateway_returns_error_body_when_prefetch_detects_embedded_stream_error",
gateway_returns_error_body_when_prefetch_detects_embedded_stream_error_impl,
);
}
async fn gateway_returns_error_body_when_prefetch_detects_embedded_stream_error_impl() {
let public_hits = Arc::new(Mutex::new(0usize));
let public_hits_clone = Arc::clone(&public_hits);
@@ -1,8 +1,8 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
strip_sse_keepalive_comments, to_bytes, AppState, Arc, Body, Bytes, HeaderName, HeaderValue,
Json, Mutex, Request, Response, Router, StatusCode, EXECUTION_PATH_EXECUTION_RUNTIME_STREAM,
EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
strip_sse_keepalive_comments, to_bytes, wait_until, AppState, Arc, Body, Bytes, HeaderName,
HeaderValue, Json, Mutex, Request, Response, Router, StatusCode,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
};
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
@@ -22,8 +22,40 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_execution_runtime_override(
const STREAM_DECISION_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_decision_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_DECISION_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream decision test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[test]
fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_execution_runtime_override()
{
run_stream_decision_test(
"gateway_executes_openai_chat_stream_via_local_decision_gate_without_execution_runtime_override",
gateway_executes_openai_chat_stream_via_local_decision_gate_without_execution_runtime_override_impl,
);
}
async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_execution_runtime_override_impl(
) {
#[derive(Debug, Clone)]
struct SeenUpstreamStreamRequest {
@@ -310,50 +342,19 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
"user-openai-local-stream-1",
),
)]));
let mut backup_candidate_row = sample_candidate_row();
backup_candidate_row.provider_id = "provider-openai-local-stream-2".to_string();
backup_candidate_row.endpoint_id = "endpoint-openai-local-stream-2".to_string();
backup_candidate_row.key_id = "key-openai-local-stream-2".to_string();
backup_candidate_row.key_name = "backup".to_string();
backup_candidate_row.key_internal_priority = 6;
backup_candidate_row.model_id = "model-openai-local-stream-2".to_string();
backup_candidate_row.global_model_id = "global-model-openai-local-stream-2".to_string();
backup_candidate_row.model_provider_model_name = "gpt-5-upstream-backup".to_string();
backup_candidate_row.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-5-upstream-backup".to_string(),
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
sample_candidate_row(),
backup_candidate_row,
]));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let mut backup_provider = sample_provider_catalog_provider();
backup_provider.id = "provider-openai-local-stream-2".to_string();
let mut backup_endpoint = sample_provider_catalog_endpoint();
backup_endpoint.id = "endpoint-openai-local-stream-2".to_string();
backup_endpoint.provider_id = "provider-openai-local-stream-2".to_string();
backup_endpoint.base_url = "https://api.openai.backup.example".to_string();
let mut backup_key = sample_provider_catalog_key();
backup_key.id = "key-openai-local-stream-2".to_string();
backup_key.provider_id = "provider-openai-local-stream-2".to_string();
backup_key.name = "backup".to_string();
backup_key.encrypted_api_key = Some(
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-upstream-openai-backup")
.expect("api key should encrypt"),
);
let (upstream_url, upstream_handle) = start_server(upstream).await;
let (provider_url, provider_handle) = start_server(provider).await;
let mut primary_endpoint = sample_provider_catalog_endpoint();
primary_endpoint.base_url = format!("{provider_url}/v1");
backup_endpoint.base_url = "http://127.0.0.1:9".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider_catalog_provider(), backup_provider],
vec![primary_endpoint, backup_endpoint],
vec![sample_provider_catalog_key(), backup_key],
vec![sample_provider_catalog_provider()],
vec![primary_endpoint],
vec![sample_provider_catalog_key()],
));
let gateway_state = AppState::new()
.expect("gateway state should build")
@@ -443,8 +444,16 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_format_candidate() {
#[test]
fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_format_candidate() {
run_stream_decision_test(
"gateway_executes_openai_chat_stream_via_local_openai_responses_cross_format_candidate",
gateway_executes_openai_chat_stream_via_local_openai_responses_cross_format_candidate_impl,
);
}
async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_format_candidate_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -821,10 +830,10 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
Arc::clone(&request_candidate_repository),
DEVELOPMENT_ENCRYPTION_KEY,
)
.with_system_config_values_for_tests(vec![(
"provider_priority_mode".to_string(),
json!("global_key"),
)]),
.with_system_config_values_for_tests(vec![
("scheduling_mode".to_string(), json!("fixed_order")),
("provider_priority_mode".to_string(), json!("global_key")),
]),
);
let gateway = build_router_with_state(gateway_state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
@@ -928,8 +937,16 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candidate_with_stream_path_rewrite(
#[test]
fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candidate_with_stream_path_rewrite(
) {
run_stream_decision_test(
"gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candidate_with_stream_path_rewrite",
gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candidate_with_stream_path_rewrite_impl,
);
}
async fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candidate_with_stream_path_rewrite_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1350,8 +1367,16 @@ async fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candi
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision_gate_with_local_stream_decision(
#[test]
fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision_gate_with_local_stream_decision(
) {
run_stream_decision_test(
"gateway_executes_openai_chat_stream_with_custom_path_via_local_decision_gate_with_local_stream_decision",
gateway_executes_openai_chat_stream_with_custom_path_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1844,8 +1869,16 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable_429_execution_runtime_status(
#[test]
fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable_429_execution_runtime_status(
) {
run_stream_decision_test(
"gateway_retries_next_local_openai_chat_stream_candidate_after_retryable_429_execution_runtime_status",
gateway_retries_next_local_openai_chat_stream_candidate_after_retryable_429_execution_runtime_status_impl,
);
}
async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable_429_execution_runtime_status_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -2024,7 +2057,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
let seen_execution_runtime_clone = Arc::clone(&seen_execution_runtime);
let seen_report = Arc::new(Mutex::new(false));
let seen_report_clone = Arc::clone(&seen_report);
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let decision_hits = Arc::new(Mutex::new(0usize));
let decision_hits_clone = Arc::clone(&decision_hits);
@@ -2106,24 +2139,26 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
let raw_body = to_bytes(body, usize::MAX).await.expect("body should read");
let payload: serde_json::Value =
serde_json::from_slice(&raw_body).expect("execution runtime payload should parse");
let mut hits = execution_runtime_hits_inner.lock().expect("mutex should lock");
*hits += 1;
let attempt = *hits;
drop(hits);
let request_url = payload
.get("url")
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let attempt = execution_runtime_hits_inner
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ 1;
seen_execution_runtime_inner.lock().expect("mutex should lock").push(
SeenExecutionRuntimeStreamRequest {
seen_execution_runtime_inner
.lock()
.expect("mutex should lock")
.push(SeenExecutionRuntimeStreamRequest {
trace_id: parts
.headers
.get(TRACE_ID_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
url: payload
.get("url")
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
url: request_url.clone(),
model: payload
.get("body")
.and_then(|value| value.get("json_body"))
@@ -2149,8 +2184,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
},
);
});
let frames = if attempt == 1 {
concat!(
@@ -2289,38 +2323,47 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
"data: {\"id\":\"chatcmpl-local-stream-failover-123\"}\n\ndata: [DONE]\n\n"
);
wait_until(5_000, || {
seen_execution_runtime
.lock()
.expect("mutex should lock")
.len()
>= 2
})
.await;
let seen_execution_runtime_requests = seen_execution_runtime
.lock()
.expect("mutex should lock")
.clone();
assert_eq!(seen_execution_runtime_requests.len(), 2);
let primary_request = seen_execution_runtime_requests
.iter()
.find(|request| request.url == "https://api.openai.primary.example/chat/completions")
.expect("primary execution runtime request should be captured");
let backup_request = seen_execution_runtime_requests
.iter()
.find(|request| request.url == "https://api.openai.backup.example/chat/completions")
.expect("backup execution runtime request should be captured");
assert_eq!(
seen_execution_runtime_requests[0].trace_id,
primary_request.trace_id,
"trace-openai-chat-local-stream-failover-123"
);
assert_eq!(primary_request.model, "gpt-5-upstream-primary");
assert!(primary_request.stream);
assert_eq!(primary_request.accept, "text/event-stream");
assert_eq!(
seen_execution_runtime_requests[0].url,
"https://api.openai.primary.example/chat/completions"
);
assert_eq!(
seen_execution_runtime_requests[0].authorization,
primary_request.authorization,
"Bearer sk-upstream-openai-primary"
);
assert_eq!(
seen_execution_runtime_requests[1].url,
"https://api.openai.backup.example/chat/completions"
backup_request.trace_id,
"trace-openai-chat-local-stream-failover-123"
);
assert_eq!(backup_request.model, "gpt-5-upstream-backup");
assert!(backup_request.stream);
assert_eq!(backup_request.accept, "text/event-stream");
assert_eq!(
seen_execution_runtime_requests[1].model,
"gpt-5-upstream-backup"
);
assert!(seen_execution_runtime_requests[1].stream);
assert_eq!(
seen_execution_runtime_requests[1].accept,
"text/event-stream"
);
assert_eq!(
seen_execution_runtime_requests[1].authorization,
backup_request.authorization,
"Bearer sk-upstream-openai-backup"
);
let stored_candidates = request_candidate_repository
@@ -2328,18 +2371,25 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
.await
.expect("request candidate trace should read");
assert_eq!(stored_candidates.len(), 2);
assert_eq!(stored_candidates[0].candidate_index, 0);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Failed);
assert_eq!(stored_candidates[0].status_code, Some(429));
let failed_candidate = stored_candidates
.iter()
.find(|candidate| candidate.candidate_index == 0)
.expect("primary failed candidate should be stored");
let success_candidate = stored_candidates
.iter()
.find(|candidate| candidate.candidate_index == 1)
.expect("backup success candidate should be stored");
assert_eq!(failed_candidate.status, RequestCandidateStatus::Failed);
assert_eq!(failed_candidate.status_code, Some(429));
assert_eq!(
stored_candidates[0].error_type.as_deref(),
failed_candidate.error_type.as_deref(),
Some("retryable_upstream_status")
);
assert_eq!(
stored_candidates[0].error_message.as_deref(),
failed_candidate.error_message.as_deref(),
Some("execution runtime stream returned retryable status 429")
);
let failed_upstream_response = stored_candidates[0]
let failed_upstream_response = failed_candidate
.extra_data
.as_ref()
.and_then(|value| value.get("upstream_response"))
@@ -2353,11 +2403,10 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
failed_upstream_response["body"]["error"]["type"],
json!("rate_limit_error")
);
assert_eq!(stored_candidates[1].candidate_index, 1);
assert_eq!(stored_candidates[1].status, RequestCandidateStatus::Success);
assert_eq!(stored_candidates[1].status_code, Some(200));
assert!(stored_candidates[1].started_at_unix_ms.is_some());
assert!(stored_candidates[1].finished_at_unix_ms.is_some());
assert_eq!(success_candidate.status, RequestCandidateStatus::Success);
assert_eq!(success_candidate.status_code, Some(200));
assert!(success_candidate.started_at_unix_ms.is_some());
assert!(success_candidate.finished_at_unix_ms.is_some());
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
assert!(
@@ -2366,7 +2415,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
);
assert_eq!(
*execution_runtime_hits.lock().expect("mutex should lock"),
execution_runtime_hits.load(std::sync::atomic::Ordering::SeqCst),
2
);
assert_eq!(*decision_hits.lock().expect("mutex should lock"), 0);
@@ -18,8 +18,39 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth_refresh() {
const STREAM_IMAGE_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_image_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_IMAGE_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream image test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[test]
fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth_refresh() {
run_stream_image_test(
"gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth_refresh",
gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth_refresh_impl,
);
}
async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth_refresh_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -438,8 +469,15 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
refresh_handle.abort();
}
#[tokio::test]
async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse() {
#[test]
fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse() {
run_stream_image_test(
"gateway_bridges_codex_image_sync_json_to_streaming_image_sse",
gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl,
);
}
async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -1057,8 +1095,16 @@ fn image_bridge_execution_runtime(
)
}
#[tokio::test]
async fn gateway_routes_openai_responses_stream_image_intent_to_openai_image_plan_without_streaming_support(
#[test]
fn gateway_routes_openai_responses_stream_image_intent_to_openai_image_plan_without_streaming_support(
) {
run_stream_image_test(
"gateway_routes_openai_responses_stream_image_intent_to_openai_image_plan_without_streaming_support",
gateway_routes_openai_responses_stream_image_intent_to_openai_image_plan_without_streaming_support_impl,
);
}
async fn gateway_routes_openai_responses_stream_image_intent_to_openai_image_plan_without_streaming_support_impl(
) {
let seen_execution_plan = Arc::new(Mutex::new(None::<SeenImageBridgeExecutionPlan>));
let execution_runtime = image_bridge_execution_runtime(Arc::clone(&seen_execution_plan));
@@ -228,8 +228,39 @@ fn collect_email_sentinel(text: &str) -> String {
text[start..end].to_string()
}
#[tokio::test]
async fn ai_execute_stream_pii_redaction_round_trip() {
const STREAM_PII_REDACTION_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_pii_redaction_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_PII_REDACTION_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream pii redaction test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[test]
fn ai_execute_stream_pii_redaction_round_trip() {
run_stream_pii_redaction_test(
"ai_execute_stream_pii_redaction_round_trip",
ai_execute_stream_pii_redaction_round_trip_impl,
);
}
async fn ai_execute_stream_pii_redaction_round_trip_impl() {
let seen_provider_request = Arc::new(Mutex::new(None::<SeenProviderStreamRequest>));
let seen_provider_request_clone = Arc::clone(&seen_provider_request);
let provider_app = Router::new().route(
@@ -1,8 +1,8 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue, Infallible, Json, Mutex, Request,
Response, Router, StatusCode, EXECUTION_PATH_EXECUTION_RUNTIME_STREAM, EXECUTION_PATH_HEADER,
TRACE_ID_HEADER,
any, build_router_with_state, build_state_with_execution_runtime_override, json,
run_stream_cli_test, start_server, to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue,
Infallible, Json, Mutex, Request, Response, Router, StatusCode,
EXECUTION_PATH_EXECUTION_RUNTIME_STREAM, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
};
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
@@ -22,8 +22,16 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision(
#[test]
fn gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision(
) {
run_stream_cli_test(
"gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_openai_responses_compact_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1,7 +1,8 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
strip_sse_keepalive_comments, to_bytes, Arc, Body, Bytes, HeaderName, HeaderValue, Infallible,
Json, Mutex, Request, Response, Router, StatusCode, UsageRuntimeConfig, TRACE_ID_HEADER,
any, build_router_with_state, build_state_with_execution_runtime_override, json,
run_stream_cli_test, start_server, strip_sse_keepalive_comments, to_bytes, Arc, Body, Bytes,
HeaderName, HeaderValue, Infallible, Json, Mutex, Request, Response, Router, StatusCode,
UsageRuntimeConfig, TRACE_ID_HEADER,
};
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
@@ -23,8 +24,15 @@ use aether_data_contracts::repository::provider_catalog::{
use aether_data_contracts::repository::usage::UsageReadRepository;
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_refresh() {
#[test]
fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_refresh() {
run_stream_cli_test(
"gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_refresh",
gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_refresh_impl,
);
}
async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_refresh_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -23,5 +23,29 @@ use super::{
GatewayFallbackReason, UsageRuntimeConfig, VideoTaskTruthSourceMode,
};
const STREAM_CLI_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_cli_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_CLI_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream cli test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
mod compact;
mod direct;
@@ -22,8 +22,39 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
#[tokio::test]
async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_candidate() {
const STREAM_PROVIDER_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_provider_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_PROVIDER_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream provider test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[test]
fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_candidate() {
run_stream_provider_test(
"gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_candidate",
gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_candidate_impl,
);
}
async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_candidate_impl() {
use base64::Engine as _;
#[derive(Debug, Clone)]
@@ -578,8 +609,16 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_waiting_for_same_format_prefetch(
#[test]
fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_waiting_for_same_format_prefetch(
) {
run_stream_provider_test(
"gateway_executes_claude_cli_stream_via_local_decision_gate_without_waiting_for_same_format_prefetch",
gateway_executes_claude_cli_stream_via_local_decision_gate_without_waiting_for_same_format_prefetch_impl,
);
}
async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_waiting_for_same_format_prefetch_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1026,8 +1065,15 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_local_stream_decision(
#[test]
fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_local_stream_decision() {
run_stream_provider_test(
"gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1558,8 +1604,16 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_stream_decision() {
#[test]
fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_stream_decision() {
run_stream_provider_test(
"gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -1,17 +1,25 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override,
encrypt_python_fernet_plaintext, json, start_server, strip_sse_keepalive_comments, to_bytes,
Arc, Body, Bytes, Digest, HeaderName, HeaderValue, InMemoryAuthApiKeySnapshotRepository,
InMemoryMinimalCandidateSelectionReadRepository, InMemoryProviderCatalogReadRepository,
InMemoryRequestCandidateRepository, Json, Mutex, Request, RequestCandidateReadRepository,
RequestCandidateStatus, Response, Router, Sha256, StatusCode, StoredAuthApiKeySnapshot,
StoredMinimalCandidateSelectionRow, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
StoredProviderCatalogProvider, StoredProviderModelMapping, DEVELOPMENT_ENCRYPTION_KEY,
TRACE_ID_HEADER,
encrypt_python_fernet_plaintext, json, run_stream_provider_gemini_test, start_server,
strip_sse_keepalive_comments, to_bytes, Arc, Body, Bytes, Digest, HeaderName, HeaderValue,
InMemoryAuthApiKeySnapshotRepository, InMemoryMinimalCandidateSelectionReadRepository,
InMemoryProviderCatalogReadRepository, InMemoryRequestCandidateRepository, Json, Mutex,
Request, RequestCandidateReadRepository, RequestCandidateStatus, Response, Router, Sha256,
StatusCode, StoredAuthApiKeySnapshot, StoredMinimalCandidateSelectionRow,
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
StoredProviderModelMapping, DEVELOPMENT_ENCRYPTION_KEY, TRACE_ID_HEADER,
};
#[tokio::test]
async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_stream_decision() {
#[test]
fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_stream_decision() {
run_stream_provider_gemini_test(
"gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -1,17 +1,25 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override,
encrypt_python_fernet_plaintext, json, start_server, strip_sse_keepalive_comments, to_bytes,
Arc, Body, Bytes, Digest, HeaderName, HeaderValue, InMemoryAuthApiKeySnapshotRepository,
InMemoryMinimalCandidateSelectionReadRepository, InMemoryProviderCatalogReadRepository,
InMemoryRequestCandidateRepository, Json, Mutex, Request, RequestCandidateReadRepository,
RequestCandidateStatus, Response, Router, Sha256, StatusCode, StoredAuthApiKeySnapshot,
StoredMinimalCandidateSelectionRow, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
StoredProviderCatalogProvider, StoredProviderModelMapping, DEVELOPMENT_ENCRYPTION_KEY,
TRACE_ID_HEADER,
encrypt_python_fernet_plaintext, json, run_stream_provider_gemini_test, start_server,
strip_sse_keepalive_comments, to_bytes, Arc, Body, Bytes, Digest, HeaderName, HeaderValue,
InMemoryAuthApiKeySnapshotRepository, InMemoryMinimalCandidateSelectionReadRepository,
InMemoryProviderCatalogReadRepository, InMemoryRequestCandidateRepository, Json, Mutex,
Request, RequestCandidateReadRepository, RequestCandidateStatus, Response, Router, Sha256,
StatusCode, StoredAuthApiKeySnapshot, StoredMinimalCandidateSelectionRow,
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
StoredProviderModelMapping, DEVELOPMENT_ENCRYPTION_KEY, TRACE_ID_HEADER,
};
#[tokio::test]
async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision() {
#[test]
fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision() {
run_stream_provider_gemini_test(
"gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -446,8 +454,15 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh() {
#[test]
fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh() {
run_stream_provider_gemini_test(
"gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh",
gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh_impl,
);
}
async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
@@ -1001,8 +1016,16 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision(
#[test]
fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision()
{
run_stream_provider_gemini_test(
"gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision",
gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision_impl,
);
}
async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_with_local_stream_decision_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -1436,8 +1459,15 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh(
#[test]
fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh() {
run_stream_provider_gemini_test(
"gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh",
gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh_impl,
);
}
async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_after_oauth_refresh_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
@@ -40,5 +40,29 @@ use aether_data_contracts::repository::provider_catalog::{
};
use sha2::{Digest, Sha256};
const STREAM_PROVIDER_GEMINI_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_stream_provider_gemini_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(STREAM_PROVIDER_GEMINI_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("stream provider gemini test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
mod local_chat;
mod local_cli;
@@ -1,11 +1,9 @@
use super::{
any, build_router_with_state, build_state_with_execution_runtime_override, json, start_server,
to_bytes, Arc, Body, Json, Mutex, Request, Router, StatusCode,
to_bytes, wait_until, Arc, Body, Json, Mutex, Request, Router, StatusCode,
EXECUTION_PATH_EXECUTION_RUNTIME_SYNC, EXECUTION_PATH_HEADER, TRACE_ID_HEADER,
};
use crate::constants::{
EXECUTION_PATH_LOCAL_API_KEY_CONCURRENCY_LIMITED, LOCAL_EXECUTION_RUNTIME_MISS_REASON_HEADER,
};
use crate::constants::LOCAL_EXECUTION_RUNTIME_MISS_REASON_HEADER;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::auth::{
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
@@ -780,6 +778,7 @@ async fn gateway_waits_for_api_key_concurrency_slot_then_executes_openai_respons
"auth_context": {
"user_id": "user-openai-cli-local-limit-123",
"api_key_id": "key-openai-cli-local-limit-123",
"api_key_concurrent_limit": 1,
"access_allowed": true
},
"public_path": "/v1/responses"
@@ -969,15 +968,15 @@ async fn gateway_waits_for_api_key_concurrency_slot_then_executes_openai_respons
}
#[test]
fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_openai_responses_sync() {
fn gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_budget_elapses() {
run_cli_sync_test(
"gateway_returns_concurrency_limited_after_wait_budget_expires_for_openai_responses_sync",
gateway_returns_concurrency_limited_after_wait_budget_expires_for_openai_responses_sync_impl,
"gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_budget_elapses",
gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_budget_elapses_impl,
);
}
async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_openai_responses_sync_impl(
) {
async fn gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_budget_elapses_impl()
{
fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
@@ -1126,36 +1125,7 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let public_hits = Arc::new(Mutex::new(0usize));
let public_hits_clone = Arc::clone(&public_hits);
let now_unix_ms = chrono::Utc::now().timestamp_millis().max(0);
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
aether_data_contracts::repository::candidates::StoredRequestCandidate::new(
"cand-pending-openai-cli-local-timeout-1".to_string(),
"req-inflight-openai-cli-local-timeout-1".to_string(),
Some("user-openai-cli-local-timeout-123".to_string()),
Some("key-openai-cli-local-timeout-123".to_string()),
Some("alice".to_string()),
Some("default".to_string()),
0,
0,
Some("provider-openai-cli-local-timeout-1".to_string()),
Some("endpoint-openai-cli-local-timeout-1".to_string()),
Some("key-openai-cli-local-timeout-1".to_string()),
RequestCandidateStatus::Pending,
None,
false,
None,
None,
None,
None,
None,
None,
None,
now_unix_ms,
Some(now_unix_ms),
None,
)
.expect("pending candidate should build"),
]));
let request_candidate_repository = Arc::new(InMemoryRequestCandidateRepository::default());
let upstream = Router::new()
.route(
@@ -1171,6 +1141,7 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
"auth_context": {
"user_id": "user-openai-cli-local-timeout-123",
"api_key_id": "key-openai-cli-local-timeout-123",
"api_key_concurrent_limit": 1,
"access_allowed": true
},
"public_path": "/v1/responses"
@@ -1204,6 +1175,7 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
*execution_runtime_hits_inner
.lock()
.expect("mutex should lock") += 1;
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
Json(json!({
"request_id": "trace-openai-cli-local-timeout-123",
"status_code": 200,
@@ -1265,8 +1237,51 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
let gateway = build_router_with_state(gateway_state);
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
let first_client = client.clone();
let first_gateway_url = gateway_url.clone();
let first_request = tokio::spawn(async move {
first_client
.post(format!("{first_gateway_url}/v1/responses"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
http::header::AUTHORIZATION,
"Bearer sk-client-openai-cli-local-timeout",
)
.header(
TRACE_ID_HEADER,
"trace-openai-cli-local-timeout-inflight-123",
)
.body("{\"model\":\"gpt-5\",\"input\":\"first\",\"store\":false}")
.send()
.await
.expect("inflight request should complete")
});
wait_until(1_000, || {
*execution_runtime_hits.lock().expect("mutex should lock") >= 1
})
.await;
let pending_deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(1_000);
loop {
let inflight_candidates = request_candidate_repository
.list_by_request_id("trace-openai-cli-local-timeout-inflight-123")
.await
.expect("inflight request candidate trace should read");
if inflight_candidates
.iter()
.any(|candidate| candidate.status == RequestCandidateStatus::Pending)
{
break;
}
assert!(
tokio::time::Instant::now() < pending_deadline,
"inflight request candidate did not become pending"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
let started_at = std::time::Instant::now();
let response = reqwest::Client::new()
let response = client
.post(format!("{gateway_url}/v1/responses"))
.header(http::header::CONTENT_TYPE, "application/json")
.header(
@@ -1281,44 +1296,40 @@ async fn gateway_returns_concurrency_limited_after_wait_budget_expires_for_opena
assert!(
started_at.elapsed() >= std::time::Duration::from_millis(100),
"request should wait for the bounded concurrency window before failing"
"request should wait for the bounded concurrency window before retrying"
);
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok()),
Some(EXECUTION_PATH_LOCAL_API_KEY_CONCURRENCY_LIMITED)
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC)
);
assert_eq!(
response
.headers()
.get(LOCAL_EXECUTION_RUNTIME_MISS_REASON_HEADER)
.and_then(|value| value.to_str().ok()),
Some("auth_api_key_concurrency_limit_reached")
None
);
let payload: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(
payload["error"]["message"],
serde_json::Value::String("当前调用方 API Key 并发请求数已达上限,请稍后重试".to_string())
);
assert_eq!(payload["model"], "gpt-5-upstream");
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-cli-local-timeout-123")
.await
.expect("request candidate trace should read");
assert_eq!(stored_candidates.len(), 1);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Skipped);
assert_eq!(
stored_candidates[0].skip_reason.as_deref(),
Some("auth_api_key_concurrency_limit_reached")
);
assert_eq!(stored_candidates[0].status, RequestCandidateStatus::Success);
assert_eq!(stored_candidates[0].skip_reason.as_deref(), None);
assert_eq!(
*execution_runtime_hits.lock().expect("mutex should lock"),
0
2
);
assert_eq!(*public_hits.lock().expect("mutex should lock"), 0);
let first_response = first_request.await.expect("inflight request should join");
assert_eq!(first_response.status(), StatusCode::OK);
gateway_handle.abort();
execution_runtime_handle.abort();
@@ -43,8 +43,15 @@ where
}
}
#[tokio::test]
async fn gateway_converts_openai_image_sync_to_gemini_image_provider() {
#[test]
fn gateway_converts_openai_image_sync_to_gemini_image_provider() {
run_image_sync_test(
"gateway_converts_openai_image_sync_to_gemini_image_provider",
gateway_converts_openai_image_sync_to_gemini_image_provider_impl,
);
}
async fn gateway_converts_openai_image_sync_to_gemini_image_provider_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
@@ -765,8 +772,15 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_refresh() {
#[test]
fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_refresh() {
run_image_sync_test(
"gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_refresh",
gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_refresh_impl,
);
}
async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_refresh_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
@@ -1271,8 +1285,15 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
refresh_handle.abort();
}
#[tokio::test]
async fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url() {
#[test]
fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url() {
run_image_sync_test(
"gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url",
gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url_impl,
);
}
async fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
+60 -8
View File
@@ -23,6 +23,30 @@ use aether_runtime_state::{
use crate::data::GatewayDataState;
const CONCURRENCY_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_concurrency_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(CONCURRENCY_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("concurrency test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn memory_runtime_semaphore(gate: &'static str, limit: usize) -> RuntimeSemaphore {
RuntimeState::memory(MemoryRuntimeStateConfig::default())
.semaphore(gate, limit, RuntimeSemaphoreConfig::default())
@@ -80,8 +104,15 @@ fn build_local_openai_gateway_state(
)
}
#[tokio::test]
async fn gateway_rejects_second_in_flight_stream_request_with_distributed_overload() {
#[test]
fn gateway_rejects_second_in_flight_stream_request_with_distributed_overload() {
run_concurrency_test(
"gateway_rejects_second_in_flight_stream_request_with_distributed_overload",
gateway_rejects_second_in_flight_stream_request_with_distributed_overload_impl,
);
}
async fn gateway_rejects_second_in_flight_stream_request_with_distributed_overload_impl() {
let execution_runtime_hits = Arc::new(AtomicUsize::new(0));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -175,8 +206,15 @@ async fn gateway_rejects_second_in_flight_stream_request_with_distributed_overlo
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_second_in_flight_stream_request_with_local_overload() {
#[test]
fn gateway_rejects_second_in_flight_stream_request_with_local_overload() {
run_concurrency_test(
"gateway_rejects_second_in_flight_stream_request_with_local_overload",
gateway_rejects_second_in_flight_stream_request_with_local_overload_impl,
);
}
async fn gateway_rejects_second_in_flight_stream_request_with_local_overload_impl() {
let execution_runtime_hits = Arc::new(AtomicUsize::new(0));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -262,8 +300,15 @@ async fn gateway_rejects_second_in_flight_stream_request_with_local_overload() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_exposes_request_concurrency_metrics() {
#[test]
fn gateway_exposes_request_concurrency_metrics() {
run_concurrency_test(
"gateway_exposes_request_concurrency_metrics",
gateway_exposes_request_concurrency_metrics_impl,
);
}
async fn gateway_exposes_request_concurrency_metrics_impl() {
let gateway = build_router_with_state(
AppState::new()
.expect("gateway state should build")
@@ -302,8 +347,15 @@ async fn gateway_exposes_request_concurrency_metrics() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_exposes_fallback_metrics() {
#[test]
fn gateway_exposes_fallback_metrics() {
run_concurrency_test(
"gateway_exposes_fallback_metrics",
gateway_exposes_fallback_metrics_impl,
);
}
async fn gateway_exposes_fallback_metrics_impl() {
let state = AppState::new().expect("gateway state should build");
let decision = sample_decision();
state.record_fallback_metric(
@@ -27,6 +27,30 @@ use crate::constants::{
};
use crate::data::GatewayDataState;
const PROVIDER_KEYS_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_provider_keys_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(PROVIDER_KEYS_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("provider keys test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
struct SummaryNullingProviderCatalogReadRepository {
inner: InMemoryProviderCatalogReadRepository,
}
@@ -879,8 +903,15 @@ async fn provider_key_concurrent_limit_reads_existing_list_response() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_fetches_allowed_models_immediately_when_creating_key_with_auto_fetch() {
#[test]
fn gateway_fetches_allowed_models_immediately_when_creating_key_with_auto_fetch() {
run_provider_keys_test(
"gateway_fetches_allowed_models_immediately_when_creating_key_with_auto_fetch",
gateway_fetches_allowed_models_immediately_when_creating_key_with_auto_fetch_impl,
);
}
async fn gateway_fetches_allowed_models_immediately_when_creating_key_with_auto_fetch_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -1759,8 +1790,15 @@ async fn provider_key_concurrent_limit_update_presence_semantics() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key_update() {
#[test]
fn gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key_update() {
run_provider_keys_test(
"gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key_update",
gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key_update_impl,
);
}
async fn gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key_update_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -1835,8 +1873,15 @@ async fn gateway_clears_allowed_models_when_disabling_auto_fetch_on_provider_key
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch() {
#[test]
fn gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch() {
run_provider_keys_test(
"gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch",
gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch_impl,
);
}
async fn gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -1948,8 +1993,16 @@ async fn gateway_overwrites_allowed_models_immediately_when_enabling_auto_fetch(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_from_empty_state() {
#[test]
fn gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_from_empty_state() {
run_provider_keys_test(
"gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_from_empty_state",
gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_from_empty_state_impl,
);
}
async fn gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_from_empty_state_impl()
{
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -2054,8 +2107,16 @@ async fn gateway_fetches_allowed_models_immediately_when_enabling_auto_fetch_fro
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_refreshes_allowed_models_when_updating_include_patterns_with_auto_fetch_enabled() {
#[test]
fn gateway_refreshes_allowed_models_when_updating_include_patterns_with_auto_fetch_enabled() {
run_provider_keys_test(
"gateway_refreshes_allowed_models_when_updating_include_patterns_with_auto_fetch_enabled",
gateway_refreshes_allowed_models_when_updating_include_patterns_with_auto_fetch_enabled_impl,
);
}
async fn gateway_refreshes_allowed_models_when_updating_include_patterns_with_auto_fetch_enabled_impl(
) {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -2158,8 +2219,16 @@ async fn gateway_refreshes_allowed_models_when_updating_include_patterns_with_au
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_refreshes_allowed_models_when_updating_exclude_patterns_with_auto_fetch_enabled() {
#[test]
fn gateway_refreshes_allowed_models_when_updating_exclude_patterns_with_auto_fetch_enabled() {
run_provider_keys_test(
"gateway_refreshes_allowed_models_when_updating_exclude_patterns_with_auto_fetch_enabled",
gateway_refreshes_allowed_models_when_updating_exclude_patterns_with_auto_fetch_enabled_impl,
);
}
async fn gateway_refreshes_allowed_models_when_updating_exclude_patterns_with_auto_fetch_enabled_impl(
) {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
File diff suppressed because it is too large Load Diff
@@ -32,16 +32,16 @@ use crate::constants::{
};
use crate::data::{GatewayDataConfig, GatewayDataState};
const SUB2API_PROVIDER_OPS_BALANCE_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
const PROVIDER_OPS_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_sub2api_provider_ops_balance_test<F, Fut>(test_name: &'static str, make_future: F)
fn run_provider_ops_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(SUB2API_PROVIDER_OPS_BALANCE_TEST_STACK_BYTES)
.stack_size(PROVIDER_OPS_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
@@ -49,7 +49,7 @@ where
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("sub2api provider ops balance test thread should spawn");
.expect("provider ops test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
@@ -117,8 +117,16 @@ fn assert_provider_ops_architectures_payload(payload: &serde_json::Value) {
assert_eq!(new_api["supported_auth_types"][0]["type"], "api_key");
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_architectures_locally_with_trusted_admin_principal_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -157,8 +165,15 @@ async fn gateway_handles_admin_provider_ops_architectures_locally_with_trusted_a
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_architectures_locally_with_bearer_admin_session() {
#[test]
fn gateway_handles_admin_provider_ops_architectures_locally_with_bearer_admin_session() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_architectures_locally_with_bearer_admin_session",
gateway_handles_admin_provider_ops_architectures_locally_with_bearer_admin_session_impl,
);
}
async fn gateway_handles_admin_provider_ops_architectures_locally_with_bearer_admin_session_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -197,8 +212,15 @@ async fn gateway_handles_admin_provider_ops_architectures_locally_with_bearer_ad
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_architecture_detail_locally_with_trusted_admin_principal(
#[test]
fn gateway_handles_admin_provider_ops_architecture_detail_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_architecture_detail_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_architecture_detail_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_architecture_detail_locally_with_trusted_admin_principal_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
@@ -241,8 +263,15 @@ async fn gateway_handles_admin_provider_ops_architecture_detail_locally_with_tru
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -328,8 +357,15 @@ async fn gateway_handles_admin_provider_ops_status_locally_with_trusted_admin_pr
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -427,8 +463,15 @@ async fn gateway_handles_admin_provider_ops_config_locally_with_trusted_admin_pr
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_principal() {
#[test]
fn gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_principal",
gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -592,8 +635,15 @@ async fn gateway_saves_admin_provider_ops_config_locally_with_trusted_admin_prin
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_principal() {
#[test]
fn gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_principal",
gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -688,8 +738,15 @@ async fn gateway_deletes_admin_provider_ops_config_locally_with_trusted_admin_pr
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_principal() {
#[test]
fn gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_principal",
gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -729,9 +786,16 @@ async fn gateway_disconnects_admin_provider_ops_locally_with_trusted_admin_princ
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_generic_api_with_trusted_admin_principal()
{
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_generic_api_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_generic_api_with_trusted_admin_principal",
gateway_verifies_admin_provider_ops_locally_for_generic_api_with_trusted_admin_principal_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_generic_api_with_trusted_admin_principal_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -830,8 +894,16 @@ async fn gateway_verifies_admin_provider_ops_locally_for_generic_api_with_truste
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_admin_principal() {
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_admin_principal",
gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_admin_principal_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_admin_principal_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -961,8 +1033,15 @@ async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_trusted_
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_auth_failure_message(
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_auth_failure_message() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_auth_failure_message",
gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_auth_failure_message_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_auth_failure_message_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
@@ -1052,8 +1131,16 @@ async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_with_cookie_a
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redirect_body_contains_arg1(
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redirect_body_contains_arg1()
{
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redirect_body_contains_arg1",
gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redirect_body_contains_arg1_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redirect_body_contains_arg1_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
@@ -1177,8 +1264,15 @@ async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_when_acw_redi
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode() {
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode",
gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -1327,8 +1421,16 @@ async fn gateway_verifies_admin_provider_ops_locally_for_anyrouter_proxy_mode()
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_runtime_http1_only() {
#[test]
fn gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_runtime_http1_only() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_runtime_http1_only",
gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_runtime_http1_only_impl,
);
}
async fn gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_runtime_http1_only_impl(
) {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -1475,8 +1577,16 @@ async fn gateway_verifies_admin_provider_ops_sub2api_proxy_mode_via_execution_ru
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_admin_principal() {
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_admin_principal",
gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_admin_principal_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_admin_principal_impl()
{
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -1616,8 +1726,15 @@ async fn gateway_verifies_admin_provider_ops_locally_for_new_api_with_trusted_ad
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode() {
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode",
gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -1741,8 +1858,15 @@ async fn gateway_verifies_admin_provider_ops_locally_for_new_api_proxy_node_mode
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_via_execution_runtime(
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_via_execution_runtime() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_via_execution_runtime",
gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_via_execution_runtime_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_via_execution_runtime_impl(
) {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
@@ -1844,8 +1968,16 @@ async fn gateway_verifies_admin_provider_ops_locally_for_new_api_without_proxy_v
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_admin_principal() {
#[test]
fn gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_admin_principal",
gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_admin_principal_impl,
);
}
async fn gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_admin_principal_impl()
{
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -1974,8 +2106,16 @@ async fn gateway_verifies_admin_provider_ops_locally_for_sub2api_with_trusted_ad
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base_url_has_path() {
#[test]
fn gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base_url_has_path() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base_url_has_path",
gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base_url_has_path_impl,
);
}
async fn gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base_url_has_path_impl()
{
let nested_refresh_hits = Arc::new(Mutex::new(0usize));
let nested_refresh_hits_clone = Arc::clone(&nested_refresh_hits);
let root_refresh_hits = Arc::new(Mutex::new(0usize));
@@ -2104,8 +2244,16 @@ async fn gateway_verifies_admin_provider_ops_sub2api_against_site_root_when_base
verify_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_without_refresh() {
#[test]
fn gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_without_refresh() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_without_refresh",
gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_without_refresh_impl,
);
}
async fn gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_without_refresh_impl()
{
let refresh_hits = Arc::new(Mutex::new(0usize));
let refresh_hits_clone = Arc::clone(&refresh_hits);
let ops = Router::new()
@@ -2233,8 +2381,15 @@ async fn gateway_verifies_admin_provider_ops_sub2api_with_cached_access_token_wi
ops_handle.abort();
}
#[tokio::test]
async fn gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_credentials() {
#[test]
fn gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_credentials() {
run_provider_ops_test(
"gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_credentials",
gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_credentials_impl,
);
}
async fn gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_credentials_impl() {
let ops = Router::new()
.route(
"/api/v1/auth/refresh",
@@ -2409,8 +2564,15 @@ async fn gateway_verifies_admin_provider_ops_sub2api_persists_rotated_runtime_cr
ops_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_principal() {
#[test]
fn gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_principal",
gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -2493,8 +2655,15 @@ async fn gateway_rejects_admin_provider_ops_connect_locally_with_trusted_admin_p
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -2624,8 +2793,15 @@ async fn gateway_handles_admin_provider_ops_balance_locally_with_trusted_admin_p
ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_balance_locally_for_generic_api_proxy_node_mode() {
#[test]
fn gateway_handles_admin_provider_ops_balance_locally_for_generic_api_proxy_node_mode() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_balance_locally_for_generic_api_proxy_node_mode",
gateway_handles_admin_provider_ops_balance_locally_for_generic_api_proxy_node_mode_impl,
);
}
async fn gateway_handles_admin_provider_ops_balance_locally_for_generic_api_proxy_node_mode_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -2774,8 +2950,16 @@ async fn gateway_handles_admin_provider_ops_balance_locally_for_generic_api_prox
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_execution_runtime() {
#[test]
fn gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_execution_runtime() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_execution_runtime",
gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_execution_runtime_impl,
);
}
async fn gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_execution_runtime_impl(
) {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -2913,8 +3097,15 @@ async fn gateway_handles_admin_provider_ops_balance_locally_without_proxy_via_ex
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_principal_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -3025,8 +3216,15 @@ async fn gateway_handles_admin_provider_ops_checkin_locally_with_trusted_admin_p
ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_proxy_node_mode() {
#[test]
fn gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_proxy_node_mode() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_proxy_node_mode",
gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_proxy_node_mode_impl,
);
}
async fn gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_proxy_node_mode_impl() {
let execution_plans = Arc::new(Mutex::new(Vec::<ExecutionPlan>::new()));
let execution_plans_clone = Arc::clone(&execution_plans);
let execution_runtime = Router::new().route(
@@ -3150,8 +3348,16 @@ async fn gateway_handles_admin_provider_ops_checkin_locally_for_generic_api_prox
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_admin_principal() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_admin_principal_impl(
) {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -3387,8 +3593,15 @@ async fn gateway_handles_admin_provider_ops_batch_balance_locally_with_trusted_a
ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_cookie() {
#[test]
fn gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_cookie() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_cookie",
gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_cookie_impl,
);
}
async fn gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_cookie_impl() {
let ops = Router::new()
.route(
"/",
@@ -3519,8 +3732,15 @@ async fn gateway_handles_admin_provider_ops_anyrouter_balance_with_auth_expired_
ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_redis() {
#[test]
fn gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_redis() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_redis",
gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_redis_impl,
);
}
async fn gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_redis_impl() {
let Some(redis) = start_managed_redis_or_skip().await else {
return;
};
@@ -3672,8 +3892,16 @@ async fn gateway_handles_admin_provider_ops_balance_cache_refresh_modes_with_red
ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_returns_live_payload_once_with_redis(
#[test]
fn gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_returns_live_payload_once_with_redis(
) {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_returns_live_payload_once_with_redis",
gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_returns_live_payload_once_with_redis_impl,
);
}
async fn gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_returns_live_payload_once_with_redis_impl(
) {
let Some(redis) = start_managed_redis_or_skip().await else {
return;
@@ -3801,8 +4029,15 @@ async fn gateway_handles_admin_provider_ops_balance_cache_miss_without_refresh_r
ops_handle.abort();
}
#[tokio::test]
async fn gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_redis() {
#[test]
fn gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_redis() {
run_provider_ops_test(
"gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_redis",
gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_redis_impl,
);
}
async fn gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_redis_impl() {
let Some(redis) = start_managed_redis_or_skip().await else {
return;
};
@@ -3989,8 +4224,15 @@ async fn gateway_clears_admin_provider_ops_balance_cache_after_config_save_with_
ops_v2_handle.abort();
}
#[tokio::test]
async fn gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_action_config() {
#[test]
fn gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_action_config() {
run_provider_ops_test(
"gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_action_config",
gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_action_config_impl,
);
}
async fn gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_action_config_impl() {
let Some(redis) = start_managed_redis_or_skip().await else {
return;
};
@@ -4222,8 +4464,15 @@ async fn gateway_verify_does_not_pollute_balance_cache_and_balance_uses_saved_ac
verify_ops_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hits_and_redis() {
#[test]
fn gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hits_and_redis() {
run_provider_ops_test(
"gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hits_and_redis",
gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hits_and_redis_impl,
);
}
async fn gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hits_and_redis_impl() {
let Some(redis) = start_managed_redis_or_skip().await else {
return;
};
@@ -4354,7 +4603,7 @@ async fn gateway_handles_admin_provider_ops_batch_balance_with_pending_cache_hit
#[test]
fn gateway_handles_admin_provider_ops_sub2api_balance_with_refresh_token_rotation() {
run_sub2api_provider_ops_balance_test(
run_provider_ops_test(
"gateway_handles_admin_provider_ops_sub2api_balance_with_refresh_token_rotation",
gateway_handles_admin_provider_ops_sub2api_balance_with_refresh_token_rotation_impl,
);
@@ -4566,7 +4815,7 @@ async fn gateway_handles_admin_provider_ops_sub2api_balance_with_refresh_token_r
#[test]
fn gateway_handles_admin_provider_ops_sub2api_balance_against_site_root_when_base_url_has_path() {
run_sub2api_provider_ops_balance_test(
run_provider_ops_test(
"gateway_handles_admin_provider_ops_sub2api_balance_against_site_root_when_base_url_has_path",
gateway_handles_admin_provider_ops_sub2api_balance_against_site_root_when_base_url_has_path_impl,
);
@@ -4782,7 +5031,7 @@ async fn gateway_handles_admin_provider_ops_sub2api_balance_against_site_root_wh
#[test]
fn gateway_handles_admin_provider_ops_sub2api_balance_with_session_login() {
run_sub2api_provider_ops_balance_test(
run_provider_ops_test(
"gateway_handles_admin_provider_ops_sub2api_balance_with_session_login",
gateway_handles_admin_provider_ops_sub2api_balance_with_session_login_impl,
);
@@ -29,6 +29,30 @@ use crate::constants::{
};
use crate::data::GatewayDataState;
const PROVIDER_QUERY_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_provider_query_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(PROVIDER_QUERY_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("provider query test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for &byte in data {
@@ -122,8 +146,15 @@ async fn assert_admin_provider_query_route(
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_fetches_upstream_for_selected_key() {
#[test]
fn gateway_handles_admin_provider_query_models_fetches_upstream_for_selected_key() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_fetches_upstream_for_selected_key",
gateway_handles_admin_provider_query_models_fetches_upstream_for_selected_key_impl,
);
}
async fn gateway_handles_admin_provider_query_models_fetches_upstream_for_selected_key_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -244,8 +275,15 @@ async fn gateway_handles_admin_provider_query_models_fetches_upstream_for_select
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs() {
#[test]
fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs",
gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs_impl,
);
}
async fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_configs_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -382,8 +420,15 @@ async fn gateway_handles_admin_provider_query_models_fetches_windsurf_model_conf
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpoint() {
#[test]
fn gateway_handles_admin_provider_query_models_with_openai_responses_endpoint() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_with_openai_responses_endpoint",
gateway_handles_admin_provider_query_models_with_openai_responses_endpoint_impl,
);
}
async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpoint_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -492,8 +537,15 @@ async fn gateway_handles_admin_provider_query_models_with_openai_responses_endpo
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated(
#[test]
fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated",
gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated_impl,
);
}
async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_when_token_invalidated_impl(
) {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
@@ -598,8 +650,15 @@ async fn gateway_handles_admin_provider_query_models_falls_back_to_codex_preset_
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_respecting_key_api_formats() {
#[test]
fn gateway_handles_admin_provider_query_models_respecting_key_api_formats() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_respecting_key_api_formats",
gateway_handles_admin_provider_query_models_respecting_key_api_formats_impl,
);
}
async fn gateway_handles_admin_provider_query_models_respecting_key_api_formats_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -729,8 +788,15 @@ async fn gateway_handles_admin_provider_query_models_respecting_key_api_formats(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_aggregating_active_keys() {
#[test]
fn gateway_handles_admin_provider_query_models_aggregating_active_keys() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_aggregating_active_keys",
gateway_handles_admin_provider_query_models_aggregating_active_keys_impl,
);
}
async fn gateway_handles_admin_provider_query_models_aggregating_active_keys_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -871,8 +937,15 @@ async fn gateway_handles_admin_provider_query_models_aggregating_active_keys() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_models_for_fixed_provider_without_endpoint() {
#[test]
fn gateway_handles_admin_provider_query_models_for_fixed_provider_without_endpoint() {
run_provider_query_test(
"gateway_handles_admin_provider_query_models_for_fixed_provider_without_endpoint",
gateway_handles_admin_provider_query_models_for_fixed_provider_without_endpoint_impl,
);
}
async fn gateway_handles_admin_provider_query_models_for_fixed_provider_without_endpoint_impl() {
let execution_runtime_hits = Arc::new(Mutex::new(0usize));
let execution_runtime_hits_clone = Arc::clone(&execution_runtime_hits);
let execution_runtime = Router::new().route(
@@ -946,8 +1019,16 @@ async fn gateway_handles_admin_provider_query_models_for_fixed_provider_without_
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_test_model_locally_with_trusted_admin_principal() {
#[test]
fn gateway_handles_admin_provider_query_test_model_locally_with_trusted_admin_principal() {
run_provider_query_test(
"gateway_handles_admin_provider_query_test_model_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_query_test_model_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_query_test_model_locally_with_trusted_admin_principal_impl()
{
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1056,8 +1137,15 @@ async fn gateway_handles_admin_provider_query_test_model_locally_with_trusted_ad
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_embedding_model_test() {
#[test]
fn gateway_handles_admin_provider_query_embedding_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_embedding_model_test",
gateway_handles_admin_provider_query_embedding_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_embedding_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1167,8 +1255,15 @@ async fn gateway_handles_admin_provider_query_embedding_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_doubao_text_embedding_model_test() {
#[test]
fn gateway_handles_admin_provider_query_doubao_text_embedding_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_doubao_text_embedding_model_test",
gateway_handles_admin_provider_query_doubao_text_embedding_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_doubao_text_embedding_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1281,8 +1376,15 @@ async fn gateway_handles_admin_provider_query_doubao_text_embedding_model_test()
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_gemini_embedding_model_test() {
#[test]
fn gateway_handles_admin_provider_query_gemini_embedding_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_gemini_embedding_model_test",
gateway_handles_admin_provider_query_gemini_embedding_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_gemini_embedding_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1396,8 +1498,15 @@ async fn gateway_handles_admin_provider_query_gemini_embedding_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test() {
#[test]
fn gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test",
gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1536,8 +1645,15 @@ async fn gateway_handles_admin_provider_query_vertex_gemini_embedding_model_test
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_jina_embedding_model_test() {
#[test]
fn gateway_handles_admin_provider_query_jina_embedding_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_jina_embedding_model_test",
gateway_handles_admin_provider_query_jina_embedding_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_jina_embedding_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1647,8 +1763,15 @@ async fn gateway_handles_admin_provider_query_jina_embedding_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_openai_rerank_model_test() {
#[test]
fn gateway_handles_admin_provider_query_openai_rerank_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_openai_rerank_model_test",
gateway_handles_admin_provider_query_openai_rerank_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_openai_rerank_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1765,8 +1888,15 @@ async fn gateway_handles_admin_provider_query_openai_rerank_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_rerank_model_test() {
#[test]
fn gateway_handles_admin_provider_query_rerank_model_test() {
run_provider_query_test(
"gateway_handles_admin_provider_query_rerank_model_test",
gateway_handles_admin_provider_query_rerank_model_test_impl,
);
}
async fn gateway_handles_admin_provider_query_rerank_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -1886,8 +2016,15 @@ async fn gateway_handles_admin_provider_query_rerank_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_maps_admin_provider_model_before_model_list_test_request() {
#[test]
fn gateway_maps_admin_provider_model_before_model_list_test_request() {
run_provider_query_test(
"gateway_maps_admin_provider_model_before_model_list_test_request",
gateway_maps_admin_provider_model_before_model_list_test_request_impl,
);
}
async fn gateway_maps_admin_provider_model_before_model_list_test_request_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2141,8 +2278,15 @@ async fn gateway_maps_admin_provider_model_before_model_list_test_request() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_test() {
#[test]
fn gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_test() {
run_provider_query_test(
"gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_test",
gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_test_impl,
);
}
async fn gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2247,8 +2391,15 @@ async fn gateway_streams_codex_openai_responses_upstream_for_admin_pool_model_te
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime() {
#[test]
fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime() {
run_provider_query_test(
"gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime",
gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime_impl,
);
}
async fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtime_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2385,8 +2536,15 @@ async fn gateway_routes_grok_responses_admin_pool_model_test_through_grok_runtim
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_streams_windsurf_connect_upstream_for_admin_model_test() {
#[test]
fn gateway_streams_windsurf_connect_upstream_for_admin_model_test() {
run_provider_query_test(
"gateway_streams_windsurf_connect_upstream_for_admin_model_test",
gateway_streams_windsurf_connect_upstream_for_admin_model_test_impl,
);
}
async fn gateway_streams_windsurf_connect_upstream_for_admin_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2501,8 +2659,15 @@ async fn gateway_streams_windsurf_connect_upstream_for_admin_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test() {
#[test]
fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test() {
run_provider_query_test(
"gateway_uses_pool_scheduler_order_for_admin_pool_model_test",
gateway_uses_pool_scheduler_order_for_admin_pool_model_test_impl,
);
}
async fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2649,8 +2814,15 @@ async fn gateway_uses_pool_scheduler_order_for_admin_pool_model_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_test_model_failover_locally_with_trusted_admin_principal(
#[test]
fn gateway_handles_admin_provider_query_test_model_failover_locally_with_trusted_admin_principal() {
run_provider_query_test(
"gateway_handles_admin_provider_query_test_model_failover_locally_with_trusted_admin_principal",
gateway_handles_admin_provider_query_test_model_failover_locally_with_trusted_admin_principal_impl,
);
}
async fn gateway_handles_admin_provider_query_test_model_failover_locally_with_trusted_admin_principal_impl(
) {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
@@ -2825,8 +2997,15 @@ async fn gateway_handles_admin_provider_query_test_model_failover_locally_with_t
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_test_model_for_kiro_locally() {
#[test]
fn gateway_handles_admin_provider_query_test_model_for_kiro_locally() {
run_provider_query_test(
"gateway_handles_admin_provider_query_test_model_for_kiro_locally",
gateway_handles_admin_provider_query_test_model_for_kiro_locally_impl,
);
}
async fn gateway_handles_admin_provider_query_test_model_for_kiro_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -2949,8 +3128,15 @@ async fn gateway_handles_admin_provider_query_test_model_for_kiro_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test() {
#[test]
fn gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test() {
run_provider_query_test(
"gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test",
gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test_impl,
);
}
async fn gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -3141,8 +3327,15 @@ async fn gateway_uses_kiro_mapped_model_name_for_explicit_model_mapping_test() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_test_model_failover_for_kiro_locally() {
#[test]
fn gateway_handles_admin_provider_query_test_model_failover_for_kiro_locally() {
run_provider_query_test(
"gateway_handles_admin_provider_query_test_model_failover_for_kiro_locally",
gateway_handles_admin_provider_query_test_model_failover_for_kiro_locally_impl,
);
}
async fn gateway_handles_admin_provider_query_test_model_failover_for_kiro_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -3298,8 +3491,15 @@ async fn gateway_handles_admin_provider_query_test_model_failover_for_kiro_local
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_retries_kiro_failover_after_http_error_without_message() {
#[test]
fn gateway_retries_kiro_failover_after_http_error_without_message() {
run_provider_query_test(
"gateway_retries_kiro_failover_after_http_error_without_message",
gateway_retries_kiro_failover_after_http_error_without_message_impl,
);
}
async fn gateway_retries_kiro_failover_after_http_error_without_message_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -3443,8 +3643,15 @@ async fn gateway_retries_kiro_failover_after_http_error_without_message() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_non_kiro_multi_model_failover_locally() {
#[test]
fn gateway_handles_non_kiro_multi_model_failover_locally() {
run_provider_query_test(
"gateway_handles_non_kiro_multi_model_failover_locally",
gateway_handles_non_kiro_multi_model_failover_locally_impl,
);
}
async fn gateway_handles_non_kiro_multi_model_failover_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -3558,8 +3765,15 @@ async fn gateway_handles_non_kiro_multi_model_failover_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_openai_responses_test_model_locally() {
#[test]
fn gateway_handles_openai_responses_test_model_locally() {
run_provider_query_test(
"gateway_handles_openai_responses_test_model_locally",
gateway_handles_openai_responses_test_model_locally_impl,
);
}
async fn gateway_handles_openai_responses_test_model_locally_impl() {
let prompt = "Tell me whether the CLI request preserved this prompt.";
let execution_runtime = Router::new().route(
"/v1/execute/sync",
@@ -3728,8 +3942,15 @@ async fn gateway_handles_openai_responses_test_model_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_openai_image_test_model_locally() {
#[test]
fn gateway_handles_openai_image_test_model_locally() {
run_provider_query_test(
"gateway_handles_openai_image_test_model_locally",
gateway_handles_openai_image_test_model_locally_impl,
);
}
async fn gateway_handles_openai_image_test_model_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -3846,8 +4067,15 @@ async fn gateway_handles_openai_image_test_model_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_reports_transport_unsupported_reason_for_non_kiro_provider() {
#[test]
fn gateway_reports_transport_unsupported_reason_for_non_kiro_provider() {
run_provider_query_test(
"gateway_reports_transport_unsupported_reason_for_non_kiro_provider",
gateway_reports_transport_unsupported_reason_for_non_kiro_provider_impl,
);
}
async fn gateway_reports_transport_unsupported_reason_for_non_kiro_provider_impl() {
let mut provider = sample_provider("provider-antigravity", "Antigravity", 10);
provider.provider_type = "antigravity".to_string();
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
@@ -3904,8 +4132,15 @@ async fn gateway_reports_transport_unsupported_reason_for_non_kiro_provider() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_handles_antigravity_endpoint_test_model_locally() {
#[test]
fn gateway_handles_antigravity_endpoint_test_model_locally() {
run_provider_query_test(
"gateway_handles_antigravity_endpoint_test_model_locally",
gateway_handles_antigravity_endpoint_test_model_locally_impl,
);
}
async fn gateway_handles_antigravity_endpoint_test_model_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4070,8 +4305,15 @@ async fn gateway_handles_antigravity_endpoint_test_model_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted() {
#[test]
fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted",
gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted_impl,
);
}
async fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4172,8 +4414,15 @@ async fn gateway_prefers_supported_non_kiro_endpoint_when_api_format_is_omitted(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_is_omitted() {
#[test]
fn gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_is_omitted",
gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_is_omitted_impl,
);
}
async fn gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_is_omitted_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4272,8 +4521,15 @@ async fn gateway_prefers_transport_supported_non_kiro_endpoint_when_api_format_i
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_api_format_is_omitted(
#[test]
fn gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_api_format_is_omitted",
gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_api_format_is_omitted_impl,
);
}
async fn gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_api_format_is_omitted_impl(
) {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
@@ -4368,8 +4624,15 @@ async fn gateway_prefers_supported_non_kiro_endpoint_with_compatible_key_when_ap
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted() {
#[test]
fn gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted",
gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted_impl,
);
}
async fn gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4463,8 +4726,16 @@ async fn gateway_uses_compatible_cli_endpoint_when_api_format_is_omitted() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_format_is_omitted() {
#[test]
fn gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_format_is_omitted",
gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_format_is_omitted_impl,
);
}
async fn gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_format_is_omitted_impl()
{
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4560,8 +4831,15 @@ async fn gateway_uses_runnable_cli_endpoint_after_chat_preference_when_api_forma
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_openai_responses_test_model_failover_locally() {
#[test]
fn gateway_handles_openai_responses_test_model_failover_locally() {
run_provider_query_test(
"gateway_handles_openai_responses_test_model_failover_locally",
gateway_handles_openai_responses_test_model_failover_locally_impl,
);
}
async fn gateway_handles_openai_responses_test_model_failover_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4653,8 +4931,15 @@ async fn gateway_handles_openai_responses_test_model_failover_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_claude_cli_test_model_locally() {
#[test]
fn gateway_handles_claude_cli_test_model_locally() {
run_provider_query_test(
"gateway_handles_claude_cli_test_model_locally",
gateway_handles_claude_cli_test_model_locally_impl,
);
}
async fn gateway_handles_claude_cli_test_model_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4744,8 +5029,15 @@ async fn gateway_handles_claude_cli_test_model_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted() {
#[test]
fn gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted",
gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted_impl,
);
}
async fn gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4831,8 +5123,15 @@ async fn gateway_uses_compatible_claude_cli_endpoint_when_api_format_is_omitted(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_claude_cli_test_model_failover_locally() {
#[test]
fn gateway_handles_claude_cli_test_model_failover_locally() {
run_provider_query_test(
"gateway_handles_claude_cli_test_model_failover_locally",
gateway_handles_claude_cli_test_model_failover_locally_impl,
);
}
async fn gateway_handles_claude_cli_test_model_failover_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -4924,8 +5223,15 @@ async fn gateway_handles_claude_cli_test_model_failover_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_gemini_cli_test_model_locally() {
#[test]
fn gateway_handles_gemini_cli_test_model_locally() {
run_provider_query_test(
"gateway_handles_gemini_cli_test_model_locally",
gateway_handles_gemini_cli_test_model_locally_impl,
);
}
async fn gateway_handles_gemini_cli_test_model_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5018,8 +5324,15 @@ async fn gateway_handles_gemini_cli_test_model_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback() {
#[test]
fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback() {
run_provider_query_test(
"gateway_handles_gemini_cli_test_model_with_oauth_header_fallback",
gateway_handles_gemini_cli_test_model_with_oauth_header_fallback_impl,
);
}
async fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5137,8 +5450,15 @@ async fn gateway_handles_gemini_cli_test_model_with_oauth_header_fallback() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_model() {
#[test]
fn gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_model() {
run_provider_query_test(
"gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_model",
gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_model_impl,
);
}
async fn gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_model_impl() {
let seen_urls = Arc::new(Mutex::new(Vec::<String>::new()));
let seen_urls_clone = Arc::clone(&seen_urls);
let execution_runtime = Router::new().route(
@@ -5306,8 +5626,15 @@ async fn gateway_hydrates_gemini_cli_project_id_from_load_code_assist_for_test_m
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted() {
#[test]
fn gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted() {
run_provider_query_test(
"gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted",
gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted_impl,
);
}
async fn gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5393,8 +5720,15 @@ async fn gateway_uses_compatible_gemini_cli_endpoint_when_api_format_is_omitted(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_gemini_cli_test_model_failover_locally() {
#[test]
fn gateway_handles_gemini_cli_test_model_failover_locally() {
run_provider_query_test(
"gateway_handles_gemini_cli_test_model_failover_locally",
gateway_handles_gemini_cli_test_model_failover_locally_impl,
);
}
async fn gateway_handles_gemini_cli_test_model_failover_locally_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5486,8 +5820,15 @@ async fn gateway_handles_gemini_cli_test_model_failover_locally() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test() {
#[test]
fn gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test() {
run_provider_query_test(
"gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test",
gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test_impl,
);
}
async fn gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5613,8 +5954,16 @@ async fn gateway_unwraps_gemini_cli_v1internal_response_for_failover_model_test(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_handles_admin_provider_query_test_model_failover_with_single_model_name_alias() {
#[test]
fn gateway_handles_admin_provider_query_test_model_failover_with_single_model_name_alias() {
run_provider_query_test(
"gateway_handles_admin_provider_query_test_model_failover_with_single_model_name_alias",
gateway_handles_admin_provider_query_test_model_failover_with_single_model_name_alias_impl,
);
}
async fn gateway_handles_admin_provider_query_test_model_failover_with_single_model_name_alias_impl(
) {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5703,8 +6052,15 @@ async fn gateway_handles_admin_provider_query_test_model_failover_with_single_mo
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_retries_non_kiro_failover_after_http_error_without_message() {
#[test]
fn gateway_retries_non_kiro_failover_after_http_error_without_message() {
run_provider_query_test(
"gateway_retries_non_kiro_failover_after_http_error_without_message",
gateway_retries_non_kiro_failover_after_http_error_without_message_impl,
);
}
async fn gateway_retries_non_kiro_failover_after_http_error_without_message_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5825,8 +6181,15 @@ async fn gateway_retries_non_kiro_failover_after_http_error_without_message() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_retries_non_kiro_failover_after_success_status_without_body() {
#[test]
fn gateway_retries_non_kiro_failover_after_success_status_without_body() {
run_provider_query_test(
"gateway_retries_non_kiro_failover_after_success_status_without_body",
gateway_retries_non_kiro_failover_after_success_status_without_body_impl,
);
}
async fn gateway_retries_non_kiro_failover_after_success_status_without_body_impl() {
let execution_runtime = Router::new().route(
"/v1/execute/sync",
any(move |Json(plan): Json<ExecutionPlan>| async move {
@@ -5953,8 +6316,15 @@ async fn gateway_retries_non_kiro_failover_after_success_status_without_body() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_preserves_non_success_status_for_test_model_local_wrapper() {
#[test]
fn gateway_preserves_non_success_status_for_test_model_local_wrapper() {
run_provider_query_test(
"gateway_preserves_non_success_status_for_test_model_local_wrapper",
gateway_preserves_non_success_status_for_test_model_local_wrapper_impl,
);
}
async fn gateway_preserves_non_success_status_for_test_model_local_wrapper_impl() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
@@ -5979,8 +6349,15 @@ async fn gateway_preserves_non_success_status_for_test_model_local_wrapper() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_provider_query_invalid_json_body() {
#[test]
fn gateway_rejects_admin_provider_query_invalid_json_body() {
run_provider_query_test(
"gateway_rejects_admin_provider_query_invalid_json_body",
gateway_rejects_admin_provider_query_invalid_json_body_impl,
);
}
async fn gateway_rejects_admin_provider_query_invalid_json_body_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().route(
@@ -6019,8 +6396,15 @@ async fn gateway_rejects_admin_provider_query_invalid_json_body() {
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_admin_provider_query_test_model_without_provider_id() {
#[test]
fn gateway_rejects_admin_provider_query_test_model_without_provider_id() {
run_provider_query_test(
"gateway_rejects_admin_provider_query_test_model_without_provider_id",
gateway_rejects_admin_provider_query_test_model_without_provider_id_impl,
);
}
async fn gateway_rejects_admin_provider_query_test_model_without_provider_id_impl() {
assert_admin_provider_query_route(
"/api/admin/provider-query/test-model",
json!({ "model": "gpt-4.1" }),
@@ -6032,8 +6416,15 @@ async fn gateway_rejects_admin_provider_query_test_model_without_provider_id() {
.await;
}
#[tokio::test]
async fn gateway_rejects_admin_provider_query_test_model_without_model() {
#[test]
fn gateway_rejects_admin_provider_query_test_model_without_model() {
run_provider_query_test(
"gateway_rejects_admin_provider_query_test_model_without_model",
gateway_rejects_admin_provider_query_test_model_without_model_impl,
);
}
async fn gateway_rejects_admin_provider_query_test_model_without_model_impl() {
assert_admin_provider_query_route(
"/api/admin/provider-query/test-model",
json!({ "provider_id": "provider-openai" }),
@@ -6045,8 +6436,15 @@ async fn gateway_rejects_admin_provider_query_test_model_without_model() {
.await;
}
#[tokio::test]
async fn gateway_rejects_admin_provider_query_test_model_failover_without_provider_id() {
#[test]
fn gateway_rejects_admin_provider_query_test_model_failover_without_provider_id() {
run_provider_query_test(
"gateway_rejects_admin_provider_query_test_model_failover_without_provider_id",
gateway_rejects_admin_provider_query_test_model_failover_without_provider_id_impl,
);
}
async fn gateway_rejects_admin_provider_query_test_model_failover_without_provider_id_impl() {
assert_admin_provider_query_route(
"/api/admin/provider-query/test-model-failover",
json!({ "failover_models": ["gpt-4.1"] }),
@@ -6058,8 +6456,15 @@ async fn gateway_rejects_admin_provider_query_test_model_failover_without_provid
.await;
}
#[tokio::test]
async fn gateway_rejects_admin_provider_query_test_model_failover_without_models() {
#[test]
fn gateway_rejects_admin_provider_query_test_model_failover_without_models() {
run_provider_query_test(
"gateway_rejects_admin_provider_query_test_model_failover_without_models",
gateway_rejects_admin_provider_query_test_model_failover_without_models_impl,
);
}
async fn gateway_rejects_admin_provider_query_test_model_failover_without_models_impl() {
assert_admin_provider_query_route(
"/api/admin/provider-query/test-model-failover",
json!({ "provider_id": "provider-openai", "failover_models": [] }),
@@ -264,8 +264,39 @@ fn sample_import_admin_user(user_id: &str) -> StoredUserAuthRecord {
.expect("admin user should build")
}
#[tokio::test]
async fn gateway_imports_admin_system_config_locally_and_persists_data() {
const ADMIN_SYSTEM_IMPORT_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_admin_system_import_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(ADMIN_SYSTEM_IMPORT_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("admin system import test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
#[test]
fn gateway_imports_admin_system_config_locally_and_persists_data() {
run_admin_system_import_test(
"gateway_imports_admin_system_config_locally_and_persists_data",
gateway_imports_admin_system_config_locally_and_persists_data_impl,
);
}
async fn gateway_imports_admin_system_config_locally_and_persists_data_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().fallback(any(move |_request: Request| {
@@ -499,8 +530,15 @@ async fn gateway_imports_admin_system_config_locally_and_persists_data() {
let _ = upstream_url;
}
#[tokio::test]
async fn gateway_imports_admin_system_config_openai_image_aliases() {
#[test]
fn gateway_imports_admin_system_config_openai_image_aliases() {
run_admin_system_import_test(
"gateway_imports_admin_system_config_openai_image_aliases",
gateway_imports_admin_system_config_openai_image_aliases_impl,
);
}
async fn gateway_imports_admin_system_config_openai_image_aliases_impl() {
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
Vec::new(),
Vec::new(),
@@ -566,8 +604,15 @@ async fn gateway_imports_admin_system_config_openai_image_aliases() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable() {
#[test]
fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable() {
run_admin_system_import_test(
"gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable",
gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable_impl,
);
}
async fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_unavailable_impl() {
let upstream_hits = Arc::new(Mutex::new(0usize));
let upstream_hits_clone = Arc::clone(&upstream_hits);
let upstream = Router::new().fallback(any(move |_request: Request| {
@@ -603,8 +648,15 @@ async fn gateway_returns_503_for_admin_system_config_import_when_local_data_is_u
let _ = upstream_url;
}
#[tokio::test]
async fn gateway_imports_legacy_admin_system_config_versions_and_model_test_succeeds() {
#[test]
fn gateway_imports_legacy_admin_system_config_versions_and_model_test_succeeds() {
run_admin_system_import_test(
"gateway_imports_legacy_admin_system_config_versions_and_model_test_succeeds",
gateway_imports_legacy_admin_system_config_versions_and_model_test_succeeds_impl,
);
}
async fn gateway_imports_legacy_admin_system_config_versions_and_model_test_succeeds_impl() {
for (
fixture_name,
expected_provider_name,
@@ -828,8 +880,15 @@ async fn assert_legacy_admin_system_config_import_model_test_succeeds(
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_unknown_admin_system_config_import_versions() {
#[test]
fn gateway_rejects_unknown_admin_system_config_import_versions() {
run_admin_system_import_test(
"gateway_rejects_unknown_admin_system_config_import_versions",
gateway_rejects_unknown_admin_system_config_import_versions_impl,
);
}
async fn gateway_rejects_unknown_admin_system_config_import_versions_impl() {
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
@@ -867,8 +926,15 @@ async fn gateway_rejects_unknown_admin_system_config_import_versions() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_imports_admin_system_users_locally_and_persists_data() {
#[test]
fn gateway_imports_admin_system_users_locally_and_persists_data() {
run_admin_system_import_test(
"gateway_imports_admin_system_users_locally_and_persists_data",
gateway_imports_admin_system_users_locally_and_persists_data_impl,
);
}
async fn gateway_imports_admin_system_users_locally_and_persists_data_impl() {
let user_wallet_updated_at = "2024-05-06T07:08:09Z";
let standalone_wallet_updated_at = "2024-06-07T08:09:10Z";
let upstream_hits = Arc::new(Mutex::new(0usize));
@@ -1142,8 +1208,15 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
let _ = upstream_url;
}
#[tokio::test]
async fn gateway_overwrites_existing_admin_system_user_key_usage_totals() {
#[test]
fn gateway_overwrites_existing_admin_system_user_key_usage_totals() {
run_admin_system_import_test(
"gateway_overwrites_existing_admin_system_user_key_usage_totals",
gateway_overwrites_existing_admin_system_user_key_usage_totals_impl,
);
}
async fn gateway_overwrites_existing_admin_system_user_key_usage_totals_impl() {
let user_key_hash = hash_api_key("sk-existing-user-key");
let standalone_key_hash = hash_api_key("sk-existing-standalone-key");
let existing_user = StoredUserAuthRecord::new(
@@ -1347,8 +1420,15 @@ async fn gateway_overwrites_existing_admin_system_user_key_usage_totals() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_imports_admin_system_config_fixture_v22() {
#[test]
fn gateway_imports_admin_system_config_fixture_v22() {
run_admin_system_import_test(
"gateway_imports_admin_system_config_fixture_v22",
gateway_imports_admin_system_config_fixture_v22_impl,
);
}
async fn gateway_imports_admin_system_config_fixture_v22_impl() {
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
@@ -1374,8 +1454,15 @@ async fn gateway_imports_admin_system_config_fixture_v22() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_imports_admin_system_config_fixtures_from_legacy_exports() {
#[test]
fn gateway_imports_admin_system_config_fixtures_from_legacy_exports() {
run_admin_system_import_test(
"gateway_imports_admin_system_config_fixtures_from_legacy_exports",
gateway_imports_admin_system_config_fixtures_from_legacy_exports_impl,
);
}
async fn gateway_imports_admin_system_config_fixtures_from_legacy_exports_impl() {
for fixture in ["v20", "v21"] {
let gateway = build_router_with_state(
AppState::new()
@@ -1409,8 +1496,15 @@ async fn gateway_imports_admin_system_config_fixtures_from_legacy_exports() {
}
}
#[tokio::test]
async fn gateway_imports_python_cli_alias_export_and_model_test_smoke() {
#[test]
fn gateway_imports_python_cli_alias_export_and_model_test_smoke() {
run_admin_system_import_test(
"gateway_imports_python_cli_alias_export_and_model_test_smoke",
gateway_imports_python_cli_alias_export_and_model_test_smoke_impl,
);
}
async fn gateway_imports_python_cli_alias_export_and_model_test_smoke_impl() {
let seen_plan = Arc::new(Mutex::new(None::<ExecutionPlan>));
let seen_plan_clone = Arc::clone(&seen_plan);
let execution_runtime = Router::new().route(
@@ -1561,8 +1655,15 @@ async fn gateway_imports_python_cli_alias_export_and_model_test_smoke() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_legacy_user_import_string_bool_field() {
#[test]
fn gateway_rejects_legacy_user_import_string_bool_field() {
run_admin_system_import_test(
"gateway_rejects_legacy_user_import_string_bool_field",
gateway_rejects_legacy_user_import_string_bool_field_impl,
);
}
async fn gateway_rejects_legacy_user_import_string_bool_field_impl() {
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::default());
let state = AppState::new()
.expect("gateway should build")
@@ -1600,8 +1701,15 @@ async fn gateway_rejects_legacy_user_import_string_bool_field() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_reports_field_path_for_invalid_admin_system_config_import_shape() {
#[test]
fn gateway_reports_field_path_for_invalid_admin_system_config_import_shape() {
run_admin_system_import_test(
"gateway_reports_field_path_for_invalid_admin_system_config_import_shape",
gateway_reports_field_path_for_invalid_admin_system_config_import_shape_impl,
);
}
async fn gateway_reports_field_path_for_invalid_admin_system_config_import_shape_impl() {
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
@@ -1641,8 +1749,15 @@ async fn gateway_reports_field_path_for_invalid_admin_system_config_import_shape
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_imports_admin_system_config_with_numeric_string_prices() {
#[test]
fn gateway_imports_admin_system_config_with_numeric_string_prices() {
run_admin_system_import_test(
"gateway_imports_admin_system_config_with_numeric_string_prices",
gateway_imports_admin_system_config_with_numeric_string_prices_impl,
);
}
async fn gateway_imports_admin_system_config_with_numeric_string_prices_impl() {
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
Vec::new(),
Vec::new(),
@@ -1714,8 +1829,15 @@ async fn gateway_imports_admin_system_config_with_numeric_string_prices() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_imports_oauth_provider_key_credentials_from_admin_system_config() {
#[test]
fn gateway_imports_oauth_provider_key_credentials_from_admin_system_config() {
run_admin_system_import_test(
"gateway_imports_oauth_provider_key_credentials_from_admin_system_config",
gateway_imports_oauth_provider_key_credentials_from_admin_system_config_impl,
);
}
async fn gateway_imports_oauth_provider_key_credentials_from_admin_system_config_impl() {
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
Vec::new(),
Vec::new(),
@@ -1804,8 +1926,15 @@ async fn gateway_imports_oauth_provider_key_credentials_from_admin_system_config
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import() {
#[test]
fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import() {
run_admin_system_import_test(
"gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import",
gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_impl,
);
}
async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_impl() {
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
Vec::new(),
Vec::new(),
@@ -1898,8 +2027,12 @@ async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_imp
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_without_refresh(
#[test]
fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_without_refresh() {
run_admin_system_import_test("gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_without_refresh", gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_without_refresh_impl);
}
async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_import_without_refresh_impl(
) {
let seen_refresh = Arc::new(Mutex::new(false));
let seen_refresh_clone = Arc::clone(&seen_refresh);
@@ -2058,8 +2191,15 @@ async fn gateway_overwrites_oauth_provider_key_credentials_from_admin_system_imp
refresh_handle.abort();
}
#[tokio::test]
async fn gateway_skips_proxy_nodes_during_admin_system_config_import() {
#[test]
fn gateway_skips_proxy_nodes_during_admin_system_config_import() {
run_admin_system_import_test(
"gateway_skips_proxy_nodes_during_admin_system_config_import",
gateway_skips_proxy_nodes_during_admin_system_config_import_impl,
);
}
async fn gateway_skips_proxy_nodes_during_admin_system_config_import_impl() {
let gateway = build_router_with_state(
AppState::new()
.expect("gateway should build")
@@ -2103,8 +2243,15 @@ async fn gateway_skips_proxy_nodes_during_admin_system_config_import() {
gateway_handle.abort();
}
#[tokio::test]
async fn gateway_preserves_manual_proxy_configs_while_skipping_proxy_nodes_during_import() {
#[test]
fn gateway_preserves_manual_proxy_configs_while_skipping_proxy_nodes_during_import() {
run_admin_system_import_test(
"gateway_preserves_manual_proxy_configs_while_skipping_proxy_nodes_during_import",
gateway_preserves_manual_proxy_configs_while_skipping_proxy_nodes_during_import_impl,
);
}
async fn gateway_preserves_manual_proxy_configs_while_skipping_proxy_nodes_during_import_impl() {
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
Vec::new(),
Vec::new(),
@@ -20,6 +20,30 @@ use crate::constants::{
};
use aether_data_contracts::repository::candidate_selection::StoredMinimalCandidateSelectionRow;
const EMBEDDING_PROXY_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_embedding_proxy_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(EMBEDDING_PROXY_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("embedding proxy test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn embedding_success_state(execution_runtime_url: String) -> AppState {
let mut snapshot =
sample_currently_usable_auth_snapshot("key-embedding-success", "user-embedding-success");
@@ -746,8 +770,15 @@ fn aliyun_embedding_execution_result(plan: &ExecutionPlan) -> ExecutionResult {
}
}
#[tokio::test]
async fn embeddings_route_accepts_openai_payload() {
#[test]
fn embeddings_route_accepts_openai_payload() {
run_embedding_proxy_test(
"embeddings_route_accepts_openai_payload",
embeddings_route_accepts_openai_payload_impl,
);
}
async fn embeddings_route_accepts_openai_payload_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(embedding_execution_runtime()).await;
let gateway = build_router_with_state(embedding_success_state(execution_runtime_url));
@@ -816,8 +847,15 @@ async fn embeddings_route_accepts_openai_payload() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_openai_payload_to_gemini_embedding_provider() {
#[test]
fn embeddings_route_converts_openai_payload_to_gemini_embedding_provider() {
run_embedding_proxy_test(
"embeddings_route_converts_openai_payload_to_gemini_embedding_provider",
embeddings_route_converts_openai_payload_to_gemini_embedding_provider_impl,
);
}
async fn embeddings_route_converts_openai_payload_to_gemini_embedding_provider_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(gemini_embedding_conversion_execution_runtime()).await;
let gateway = build_router_with_state(gemini_embedding_success_state(
@@ -867,8 +905,15 @@ async fn embeddings_route_converts_openai_payload_to_gemini_embedding_provider()
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_provider() {
#[test]
fn embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_provider() {
run_embedding_proxy_test(
"embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_provider",
embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_provider_impl,
);
}
async fn embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_provider_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(vertex_gemini_embedding_conversion_execution_runtime()).await;
let gateway =
@@ -911,8 +956,15 @@ async fn embeddings_route_converts_openai_payload_to_vertex_gemini_embedding_pro
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint() {
#[test]
fn embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint() {
run_embedding_proxy_test(
"embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint",
embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint_impl,
);
}
async fn embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(gemini_embedding_batch_conversion_execution_runtime()).await;
let gateway = build_router_with_state(gemini_embedding_success_state(
@@ -957,8 +1009,15 @@ async fn embeddings_route_converts_openai_batch_payload_to_gemini_batch_endpoint
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_text_payload_to_aliyun_embedding_provider() {
#[test]
fn embeddings_route_converts_text_payload_to_aliyun_embedding_provider() {
run_embedding_proxy_test(
"embeddings_route_converts_text_payload_to_aliyun_embedding_provider",
embeddings_route_converts_text_payload_to_aliyun_embedding_provider_impl,
);
}
async fn embeddings_route_converts_text_payload_to_aliyun_embedding_provider_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(aliyun_embedding_conversion_execution_runtime(
json!([{ "text": "hello" }]),
@@ -1006,8 +1065,15 @@ async fn embeddings_route_converts_text_payload_to_aliyun_embedding_provider() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provider() {
#[test]
fn embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provider() {
run_embedding_proxy_test(
"embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provider",
embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provider_impl,
);
}
async fn embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provider_impl() {
let expected_contents = json!([
{ "text": "white running shoes" },
{ "image": "https://example.com/shoe.png" },
@@ -1049,8 +1115,15 @@ async fn embeddings_route_converts_multimodal_payload_to_aliyun_embedding_provid
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_skips_openai_candidate_for_multimodal_payload() {
#[test]
fn embeddings_route_skips_openai_candidate_for_multimodal_payload() {
run_embedding_proxy_test(
"embeddings_route_skips_openai_candidate_for_multimodal_payload",
embeddings_route_skips_openai_candidate_for_multimodal_payload_impl,
);
}
async fn embeddings_route_skips_openai_candidate_for_multimodal_payload_impl() {
let expected_contents = json!([
{ "text": "white running shoes" },
{ "image": "https://example.com/shoe.png" }
@@ -1090,8 +1163,15 @@ async fn embeddings_route_skips_openai_candidate_for_multimodal_payload() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider() {
#[test]
fn embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider() {
run_embedding_proxy_test(
"embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider",
embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider_impl,
);
}
async fn embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider_impl() {
let expected_contents = json!([
{
"text": "white running shoes",
@@ -1134,8 +1214,15 @@ async fn embeddings_route_converts_fusion_payload_to_aliyun_embedding_provider()
execution_runtime_handle.abort();
}
#[tokio::test]
async fn gemini_embed_content_route_uses_native_gemini_embedding_provider() {
#[test]
fn gemini_embed_content_route_uses_native_gemini_embedding_provider() {
run_embedding_proxy_test(
"gemini_embed_content_route_uses_native_gemini_embedding_provider",
gemini_embed_content_route_uses_native_gemini_embedding_provider_impl,
);
}
async fn gemini_embed_content_route_uses_native_gemini_embedding_provider_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(gemini_embedding_native_execution_runtime()).await;
let gateway = build_router_with_state(gemini_embedding_success_state(
@@ -1188,8 +1275,15 @@ async fn gemini_embed_content_route_uses_native_gemini_embedding_provider() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_accepts_all_canonical_input_shapes() {
#[test]
fn embeddings_route_accepts_all_canonical_input_shapes() {
run_embedding_proxy_test(
"embeddings_route_accepts_all_canonical_input_shapes",
embeddings_route_accepts_all_canonical_input_shapes_impl,
);
}
async fn embeddings_route_accepts_all_canonical_input_shapes_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(embedding_execution_runtime()).await;
let gateway = build_router_with_state(embedding_success_state(execution_runtime_url));
@@ -1229,8 +1323,15 @@ async fn embeddings_route_accepts_all_canonical_input_shapes() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn embeddings_route_rejects_invalid_local_payloads() {
#[test]
fn embeddings_route_rejects_invalid_local_payloads() {
run_embedding_proxy_test(
"embeddings_route_rejects_invalid_local_payloads",
embeddings_route_rejects_invalid_local_payloads_impl,
);
}
async fn embeddings_route_rejects_invalid_local_payloads_impl() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
@@ -1294,8 +1395,15 @@ async fn embeddings_route_rejects_invalid_local_payloads() {
gateway_handle.abort();
}
#[tokio::test]
async fn embeddings_route_rejects_non_json_content_type() {
#[test]
fn embeddings_route_rejects_non_json_content_type() {
run_embedding_proxy_test(
"embeddings_route_rejects_non_json_content_type",
embeddings_route_rejects_non_json_content_type_impl,
);
}
async fn embeddings_route_rejects_non_json_content_type_impl() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
@@ -1317,8 +1425,15 @@ async fn embeddings_route_rejects_non_json_content_type() {
gateway_handle.abort();
}
#[tokio::test]
async fn embeddings_route_rejects_chat_only_model() {
#[test]
fn embeddings_route_rejects_chat_only_model() {
run_embedding_proxy_test(
"embeddings_route_rejects_chat_only_model",
embeddings_route_rejects_chat_only_model_impl,
);
}
async fn embeddings_route_rejects_chat_only_model_impl() {
let mut snapshot = sample_currently_usable_auth_snapshot("key-embedding-1", "user-embedding-1");
snapshot.user_allowed_api_formats = Some(vec!["openai:embedding".to_string()]);
snapshot.api_key_allowed_api_formats = Some(vec!["openai:embedding".to_string()]);
@@ -1366,8 +1481,15 @@ async fn embeddings_route_rejects_chat_only_model() {
gateway_handle.abort();
}
#[tokio::test]
async fn embeddings_route_rejects_chat_only_api_format() {
#[test]
fn embeddings_route_rejects_chat_only_api_format() {
run_embedding_proxy_test(
"embeddings_route_rejects_chat_only_api_format",
embeddings_route_rejects_chat_only_api_format_impl,
);
}
async fn embeddings_route_rejects_chat_only_api_format_impl() {
let mut snapshot = sample_currently_usable_auth_snapshot("key-embedding-2", "user-embedding-2");
snapshot.user_allowed_api_formats = Some(vec!["openai:chat".to_string()]);
snapshot.api_key_allowed_api_formats = Some(vec!["openai:chat".to_string()]);
@@ -20,6 +20,30 @@ use crate::constants::{
EXECUTION_PATH_LOCAL_AUTH_DENIED,
};
const RERANK_PROXY_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_rerank_proxy_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(RERANK_PROXY_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("rerank proxy test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn rerank_success_state(execution_runtime_url: String) -> AppState {
let mut snapshot =
sample_currently_usable_auth_snapshot("key-rerank-success", "user-rerank-success");
@@ -142,8 +166,15 @@ fn rerank_execution_result(plan: &ExecutionPlan) -> ExecutionResult {
}
}
#[tokio::test]
async fn rerank_route_accepts_openai_payload() {
#[test]
fn rerank_route_accepts_openai_payload() {
run_rerank_proxy_test(
"rerank_route_accepts_openai_payload",
rerank_route_accepts_openai_payload_impl,
);
}
async fn rerank_route_accepts_openai_payload_impl() {
let (execution_runtime_url, execution_runtime_handle) =
start_server(rerank_execution_runtime()).await;
let gateway = build_router_with_state(rerank_success_state(execution_runtime_url));
@@ -200,8 +231,15 @@ async fn rerank_route_accepts_openai_payload() {
execution_runtime_handle.abort();
}
#[tokio::test]
async fn rerank_route_rejects_invalid_local_payloads() {
#[test]
fn rerank_route_rejects_invalid_local_payloads() {
run_rerank_proxy_test(
"rerank_route_rejects_invalid_local_payloads",
rerank_route_rejects_invalid_local_payloads_impl,
);
}
async fn rerank_route_rejects_invalid_local_payloads_impl() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
let client = reqwest::Client::new();
@@ -257,8 +295,15 @@ async fn rerank_route_rejects_invalid_local_payloads() {
gateway_handle.abort();
}
#[tokio::test]
async fn rerank_route_rejects_non_json_content_type() {
#[test]
fn rerank_route_rejects_non_json_content_type() {
run_rerank_proxy_test(
"rerank_route_rejects_non_json_content_type",
rerank_route_rejects_non_json_content_type_impl,
);
}
async fn rerank_route_rejects_non_json_content_type_impl() {
let gateway = build_router_with_state(AppState::new().expect("gateway should build"));
let (gateway_url, gateway_handle) = start_server(gateway).await;
@@ -280,8 +325,15 @@ async fn rerank_route_rejects_non_json_content_type() {
gateway_handle.abort();
}
#[tokio::test]
async fn rerank_route_rejects_chat_only_api_format() {
#[test]
fn rerank_route_rejects_chat_only_api_format() {
run_rerank_proxy_test(
"rerank_route_rejects_chat_only_api_format",
rerank_route_rejects_chat_only_api_format_impl,
);
}
async fn rerank_route_rejects_chat_only_api_format_impl() {
let mut snapshot = sample_currently_usable_auth_snapshot("key-rerank-2", "user-rerank-2");
snapshot.user_allowed_api_formats = Some(vec!["openai:chat".to_string()]);
snapshot.api_key_allowed_api_formats = Some(vec!["openai:chat".to_string()]);
+65 -10
View File
@@ -26,6 +26,30 @@ mod registry_cleanup;
mod stream;
mod sync;
const FILES_TEST_STACK_BYTES: usize = 16 * 1024 * 1024;
fn run_files_test<F, Fut>(test_name: &'static str, make_future: F)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + 'static,
{
let handle = std::thread::Builder::new()
.name(test_name.to_string())
.stack_size(FILES_TEST_STACK_BYTES)
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime should build");
runtime.block_on(make_future());
})
.expect("files test thread should spawn");
if let Err(payload) = handle.join() {
std::panic::resume_unwind(payload);
}
}
fn hash_api_key(value: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(value.as_bytes());
@@ -167,8 +191,13 @@ fn sample_files_provider_catalog_key() -> StoredProviderCatalogKey {
.expect("key transport should build")
}
#[tokio::test]
async fn gateway_locally_denies_gemini_files_download_control_sync_even_with_opt_in_headers_when_execution_runtime_missing(
#[test]
fn gateway_locally_denies_gemini_files_download_control_sync_even_with_opt_in_headers_when_execution_runtime_missing(
) {
run_files_test("gateway_locally_denies_gemini_files_download_control_sync_even_with_opt_in_headers_when_execution_runtime_missing", gateway_locally_denies_gemini_files_download_control_sync_even_with_opt_in_headers_when_execution_runtime_missing_impl);
}
async fn gateway_locally_denies_gemini_files_download_control_sync_even_with_opt_in_headers_when_execution_runtime_missing_impl(
) {
let execute_hits = Arc::new(Mutex::new(0usize));
let execute_hits_clone = Arc::clone(&execute_hits);
@@ -237,8 +266,13 @@ async fn gateway_locally_denies_gemini_files_download_control_sync_even_with_opt
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_locally_denies_gemini_files_download_control_sync_without_opt_in_header_when_execution_runtime_missing(
#[test]
fn gateway_locally_denies_gemini_files_download_control_sync_without_opt_in_header_when_execution_runtime_missing(
) {
run_files_test("gateway_locally_denies_gemini_files_download_control_sync_without_opt_in_header_when_execution_runtime_missing", gateway_locally_denies_gemini_files_download_control_sync_without_opt_in_header_when_execution_runtime_missing_impl);
}
async fn gateway_locally_denies_gemini_files_download_control_sync_without_opt_in_header_when_execution_runtime_missing_impl(
) {
let execute_hits = Arc::new(Mutex::new(0usize));
let execute_hits_clone = Arc::clone(&execute_hits);
@@ -324,8 +358,15 @@ async fn gateway_locally_denies_gemini_files_download_control_sync_without_opt_i
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_skips_gemini_files_download_control_sync_without_opt_in_header() {
#[test]
fn gateway_skips_gemini_files_download_control_sync_without_opt_in_header() {
run_files_test(
"gateway_skips_gemini_files_download_control_sync_without_opt_in_header",
gateway_skips_gemini_files_download_control_sync_without_opt_in_header_impl,
);
}
async fn gateway_skips_gemini_files_download_control_sync_without_opt_in_header_impl() {
let execute_hits = Arc::new(Mutex::new(0usize));
let execute_hits_clone = Arc::clone(&execute_hits);
let public_hits = Arc::new(Mutex::new(0usize));
@@ -388,8 +429,15 @@ async fn gateway_skips_gemini_files_download_control_sync_without_opt_in_header(
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_gemini_files_get_via_local_decision_gate_with_local_planning_only() {
#[test]
fn gateway_executes_gemini_files_get_via_local_decision_gate_with_local_planning_only() {
run_files_test(
"gateway_executes_gemini_files_get_via_local_decision_gate_with_local_planning_only",
gateway_executes_gemini_files_get_via_local_decision_gate_with_local_planning_only_impl,
);
}
async fn gateway_executes_gemini_files_get_via_local_decision_gate_with_local_planning_only_impl() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
method: String,
@@ -596,8 +644,15 @@ async fn gateway_executes_gemini_files_get_via_local_decision_gate_with_local_pl
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_rejects_non_post_gemini_upload_without_hitting_fallback_probe() {
#[test]
fn gateway_rejects_non_post_gemini_upload_without_hitting_fallback_probe() {
run_files_test(
"gateway_rejects_non_post_gemini_upload_without_hitting_fallback_probe",
gateway_rejects_non_post_gemini_upload_without_hitting_fallback_probe_impl,
);
}
async fn gateway_rejects_non_post_gemini_upload_without_hitting_fallback_probe_impl() {
let public_hits = Arc::new(Mutex::new(0usize));
let public_hits_clone = Arc::clone(&public_hits);
@@ -3,8 +3,15 @@ use aether_data::repository::gemini_file_mappings::{
GeminiFileMappingReadRepository, InMemoryGeminiFileMappingRepository, StoredGeminiFileMapping,
};
#[tokio::test]
async fn gateway_background_gemini_file_mapping_cleanup_deletes_expired_entries() {
#[test]
fn gateway_background_gemini_file_mapping_cleanup_deletes_expired_entries() {
super::run_files_test(
"gateway_background_gemini_file_mapping_cleanup_deletes_expired_entries",
gateway_background_gemini_file_mapping_cleanup_deletes_expired_entries_impl,
);
}
async fn gateway_background_gemini_file_mapping_cleanup_deletes_expired_entries_impl() {
fn sample_mapping(
id: &str,
file_name: &str,
+23 -6
View File
@@ -14,8 +14,13 @@ use super::{
TRACE_ID_HEADER,
};
#[tokio::test]
async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_local_planning_only() {
#[test]
fn gateway_executes_gemini_files_download_via_local_decision_gate_with_local_planning_only() {
super::run_files_test("gateway_executes_gemini_files_download_via_local_decision_gate_with_local_planning_only", gateway_executes_gemini_files_download_via_local_decision_gate_with_local_planning_only_impl);
}
async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_local_planning_only_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeStreamRequest {
method: String,
@@ -280,8 +285,13 @@ async fn gateway_executes_gemini_files_download_via_local_decision_gate_with_loc
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_headers_when_execution_runtime_missing(
#[test]
fn gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_headers_when_execution_runtime_missing(
) {
super::run_files_test("gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_headers_when_execution_runtime_missing", gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_headers_when_execution_runtime_missing_impl);
}
async fn gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_headers_when_execution_runtime_missing_impl(
) {
let execute_hits = Arc::new(Mutex::new(0usize));
let execute_hits_clone = Arc::clone(&execute_hits);
@@ -351,8 +361,15 @@ async fn gateway_locally_denies_gemini_files_upload_control_sync_with_opt_in_hea
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_locally_denies_gemini_files_upload_control_sync_without_opt_in_header() {
#[test]
fn gateway_locally_denies_gemini_files_upload_control_sync_without_opt_in_header() {
super::run_files_test(
"gateway_locally_denies_gemini_files_upload_control_sync_without_opt_in_header",
gateway_locally_denies_gemini_files_upload_control_sync_without_opt_in_header_impl,
);
}
async fn gateway_locally_denies_gemini_files_upload_control_sync_without_opt_in_header_impl() {
let execute_hits = Arc::new(Mutex::new(0usize));
let execute_hits_clone = Arc::clone(&execute_hits);
let public_hits = Arc::new(Mutex::new(0usize));
+30 -6
View File
@@ -10,8 +10,16 @@ use super::{
RequestCandidateStatus, Router, StatusCode, DEVELOPMENT_ENCRYPTION_KEY, TRACE_ID_HEADER,
};
#[tokio::test]
async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local_planning_only() {
#[test]
fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local_planning_only() {
super::run_files_test(
"gateway_executes_gemini_files_upload_via_local_decision_gate_with_local_planning_only",
gateway_executes_gemini_files_upload_via_local_decision_gate_with_local_planning_only_impl,
);
}
async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local_planning_only_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
method: String,
@@ -300,8 +308,16 @@ async fn gateway_executes_gemini_files_upload_via_local_decision_gate_with_local
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_gemini_files_list_via_local_decision_gate_with_local_planning_only() {
#[test]
fn gateway_executes_gemini_files_list_via_local_decision_gate_with_local_planning_only() {
super::run_files_test(
"gateway_executes_gemini_files_list_via_local_decision_gate_with_local_planning_only",
gateway_executes_gemini_files_list_via_local_decision_gate_with_local_planning_only_impl,
);
}
async fn gateway_executes_gemini_files_list_via_local_decision_gate_with_local_planning_only_impl()
{
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
method: String,
@@ -508,8 +524,16 @@ async fn gateway_executes_gemini_files_list_via_local_decision_gate_with_local_p
upstream_handle.abort();
}
#[tokio::test]
async fn gateway_executes_gemini_files_delete_via_local_decision_gate_with_local_planning_only() {
#[test]
fn gateway_executes_gemini_files_delete_via_local_decision_gate_with_local_planning_only() {
super::run_files_test(
"gateway_executes_gemini_files_delete_via_local_decision_gate_with_local_planning_only",
gateway_executes_gemini_files_delete_via_local_decision_gate_with_local_planning_only_impl,
);
}
async fn gateway_executes_gemini_files_delete_via_local_decision_gate_with_local_planning_only_impl(
) {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
method: String,
+350 -40
View File
@@ -1,8 +1,11 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use aether_contracts::ExecutionPlan;
use aether_runtime::{ConcurrencyGate, ConcurrencyPermit, MetricKind, MetricLabel, MetricSample};
use aether_runtime::{
ConcurrencyError, ConcurrencyGate, ConcurrencyPermit, MetricKind, MetricLabel, MetricSample,
};
use dashmap::DashMap;
use tokio::time::timeout;
use url::Url;
@@ -13,12 +16,15 @@ use crate::GatewayError;
const GATE_NAME: &str = "gateway_upstream_target";
const DEFAULT_METRIC_TARGET_LIMIT: usize = 32;
const METRIC_TARGET_LIMIT_ENV: &str = "AETHER_GATEWAY_UPSTREAM_TARGET_GATE_METRIC_LIMIT";
const TARGET_QUEUE_BUDGET_MS_ENV: &str = "AETHER_GATEWAY_UPSTREAM_TARGET_GATE_QUEUE_BUDGET_MS";
const DEFAULT_TARGET_QUEUE_BUDGET_MS: u64 = 1;
const MAX_TARGET_QUEUE_BUDGET_MS: u64 = 5_000;
#[derive(Debug)]
pub(crate) struct UpstreamTargetAdmission {
limit: Option<usize>,
queue_budget: Duration,
gates: DashMap<String, Arc<ConcurrencyGate>>,
gates: DashMap<String, Arc<UpstreamTargetGate>>,
}
#[derive(Debug)]
@@ -26,11 +32,62 @@ pub(crate) struct UpstreamTargetAdmissionPermit {
_permit: ConcurrencyPermit,
}
#[derive(Debug)]
struct UpstreamTargetGate {
gate: ConcurrencyGate,
raw_seen_total: AtomicU64,
preselect_total: AtomicU64,
selected_total: AtomicU64,
saturated_total: AtomicU64,
}
impl UpstreamTargetGate {
fn new(limit: usize) -> Self {
Self {
gate: ConcurrencyGate::new(GATE_NAME, limit),
raw_seen_total: AtomicU64::new(0),
preselect_total: AtomicU64::new(0),
selected_total: AtomicU64::new(0),
saturated_total: AtomicU64::new(0),
}
}
fn raw_seen(&self) {
self.raw_seen_total.fetch_add(1, Ordering::Relaxed);
}
fn preselected(&self) {
self.preselect_total.fetch_add(1, Ordering::Relaxed);
}
fn selected(&self) {
self.selected_total.fetch_add(1, Ordering::Relaxed);
}
fn saturated(&self) {
self.saturated_total.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Debug, Clone)]
pub(crate) struct UpstreamTargetAdmissionSnapshot {
pub(crate) target: String,
pub(crate) in_flight: usize,
pub(crate) available_permits: usize,
pub(crate) high_watermark: usize,
pub(crate) rejected: u64,
pub(crate) raw_seen_total: u64,
pub(crate) preselect_total: u64,
pub(crate) selected_total: u64,
pub(crate) selection_pressure_total: u64,
pub(crate) saturated_total: u64,
}
impl UpstreamTargetAdmission {
pub(crate) fn new(limit: Option<usize>, queue_budget: Duration) -> Self {
Self {
limit,
queue_budget,
queue_budget: target_queue_budget(queue_budget),
gates: DashMap::new(),
}
}
@@ -47,13 +104,15 @@ impl UpstreamTargetAdmission {
let gate = self
.gates
.entry(key.clone())
.or_insert_with(|| Arc::new(ConcurrencyGate::new(GATE_NAME, limit)))
.or_insert_with(|| Arc::new(UpstreamTargetGate::new(limit)))
.clone();
gate.selected();
let started_at = Instant::now();
let permit = match timeout(self.queue_budget, gate.acquire()).await {
let permit = match timeout(self.queue_budget, gate.gate.acquire()).await {
Ok(Ok(permit)) => permit,
Ok(Err(err)) => return Err(GatewayError::Internal(err.to_string())),
Err(_) => {
gate.saturated();
tracing::debug!(
event_name = "gateway_upstream_target_admission_timeout",
log_type = "ops",
@@ -77,6 +136,72 @@ impl UpstreamTargetAdmission {
Ok(Some(UpstreamTargetAdmissionPermit { _permit: permit }))
}
pub(crate) fn try_acquire_for_plan(
&self,
plan: &ExecutionPlan,
) -> Result<Option<UpstreamTargetAdmissionPermit>, GatewayError> {
let Some(limit) = self.limit else {
return Ok(None);
};
let key = upstream_target_key(plan);
let gate = self
.gates
.entry(key)
.or_insert_with(|| Arc::new(UpstreamTargetGate::new(limit)))
.clone();
gate.selected();
match gate.gate.try_acquire() {
Ok(permit) => Ok(Some(UpstreamTargetAdmissionPermit { _permit: permit })),
Err(ConcurrencyError::Saturated { .. }) => {
gate.saturated();
Ok(None)
}
Err(err) => Err(GatewayError::Internal(err.to_string())),
}
}
pub(crate) fn snapshot_for_plan(
&self,
plan: &ExecutionPlan,
) -> Option<UpstreamTargetAdmissionSnapshot> {
let key = upstream_target_key(plan);
self.snapshot_for_target_key(&key)
}
pub(crate) fn snapshot_for_target_key(
&self,
target: &str,
) -> Option<UpstreamTargetAdmissionSnapshot> {
let entry = self.gates.get(target)?;
Some(snapshot_for_gate(target.to_string(), entry.value()))
}
pub(crate) fn record_preselect_for_target_key(&self, target: &str) {
let Some(limit) = self.limit else {
return;
};
let gate = self
.gates
.entry(target.to_string())
.or_insert_with(|| Arc::new(UpstreamTargetGate::new(limit)));
gate.preselected();
}
pub(crate) fn record_raw_seen_for_target_key(&self, target: &str) {
let Some(limit) = self.limit else {
return;
};
let gate = self
.gates
.entry(target.to_string())
.or_insert_with(|| Arc::new(UpstreamTargetGate::new(limit)));
gate.raw_seen();
}
pub(crate) fn limit(&self) -> Option<usize> {
self.limit
}
pub(crate) fn metric_samples(&self) -> Vec<MetricSample> {
let mut samples = vec![MetricSample::new(
"upstream_target_gate_active_targets",
@@ -99,36 +224,25 @@ impl UpstreamTargetAdmission {
let mut snapshots = self
.gates
.iter()
.map(|entry| {
let snapshot = entry.value().snapshot();
(
entry.key().clone(),
snapshot.in_flight,
snapshot.available_permits,
snapshot.high_watermark,
snapshot.rejected,
)
})
.map(|entry| snapshot_for_gate(entry.key().clone(), entry.value()))
.collect::<Vec<_>>();
snapshots.sort_by(|left, right| {
right
.1
.cmp(&left.1)
.then_with(|| right.3.cmp(&left.3))
.then_with(|| right.4.cmp(&left.4))
.in_flight
.cmp(&left.in_flight)
.then_with(|| right.high_watermark.cmp(&left.high_watermark))
.then_with(|| right.saturated_total.cmp(&left.saturated_total))
});
let metric_target_limit = upstream_target_metric_limit();
for (target, in_flight, available, high_watermark, rejected) in
snapshots.into_iter().take(metric_target_limit)
{
let labels = vec![MetricLabel::new("target", target)];
for snapshot in snapshots.into_iter().take(metric_target_limit) {
let labels = vec![MetricLabel::new("target", snapshot.target)];
samples.push(
MetricSample::new(
"upstream_target_gate_in_flight",
"Current number of in-flight operations for an upstream target admission gate.",
MetricKind::Gauge,
in_flight as u64,
snapshot.in_flight as u64,
)
.with_labels(labels.clone()),
);
@@ -137,7 +251,7 @@ impl UpstreamTargetAdmission {
"upstream_target_gate_available_permits",
"Currently available permits for an upstream target admission gate.",
MetricKind::Gauge,
available as u64,
snapshot.available_permits as u64,
)
.with_labels(labels.clone()),
);
@@ -146,7 +260,7 @@ impl UpstreamTargetAdmission {
"upstream_target_gate_high_watermark",
"Highest observed in-flight count for an upstream target admission gate.",
MetricKind::Gauge,
high_watermark as u64,
snapshot.high_watermark as u64,
)
.with_labels(labels.clone()),
);
@@ -155,7 +269,61 @@ impl UpstreamTargetAdmission {
"upstream_target_gate_rejected_total",
"Number of operations rejected by an upstream target admission gate.",
MetricKind::Counter,
rejected,
snapshot.rejected,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_selected_total",
"Number of selections for an upstream target.",
MetricKind::Counter,
snapshot.selected_total,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_raw_seen_total",
"Number of lightweight target-selection windows where an upstream target appeared.",
MetricKind::Counter,
snapshot.raw_seen_total,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_preselect_total",
"Number of lightweight pre-first-byte selections for an upstream target.",
MetricKind::Counter,
snapshot.preselect_total,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_in_flight",
"Current number of pre-first-byte in-flight operations for an upstream target.",
MetricKind::Gauge,
snapshot.in_flight as u64,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_max_in_flight",
"Highest observed pre-first-byte in-flight count for an upstream target.",
MetricKind::Gauge,
snapshot.high_watermark as u64,
)
.with_labels(labels.clone()),
);
samples.push(
MetricSample::new(
"upstream_target_saturated_total",
"Number of saturated selections for an upstream target.",
MetricKind::Counter,
snapshot.saturated_total,
)
.with_labels(labels),
);
@@ -165,27 +333,57 @@ impl UpstreamTargetAdmission {
}
}
fn snapshot_for_gate(target: String, gate: &UpstreamTargetGate) -> UpstreamTargetAdmissionSnapshot {
let snapshot = gate.gate.snapshot();
let raw_seen_total = gate.raw_seen_total.load(Ordering::Relaxed);
let preselect_total = gate.preselect_total.load(Ordering::Relaxed);
let selected_total = gate.selected_total.load(Ordering::Relaxed);
UpstreamTargetAdmissionSnapshot {
target,
in_flight: snapshot.in_flight,
available_permits: snapshot.available_permits,
high_watermark: snapshot.high_watermark,
rejected: snapshot.rejected,
raw_seen_total,
preselect_total,
selected_total,
selection_pressure_total: preselect_total.saturating_add(selected_total),
saturated_total: gate.saturated_total.load(Ordering::Relaxed),
}
}
pub(crate) fn upstream_target_key(plan: &ExecutionPlan) -> String {
let parsed = Url::parse(plan.url.as_str()).ok();
let Some(url) = parsed else {
return fallback_target_key(plan);
};
let scheme = url.scheme().to_ascii_lowercase();
let Some(host) = url.host_str().map(|host| host.to_ascii_lowercase()) else {
return fallback_target_key(plan);
};
let port = url
.port_or_known_default()
.map(|port| port.to_string())
.unwrap_or_else(|| "-".to_string());
let proxy = plan
.proxy
.as_ref()
.and_then(|proxy| proxy.url.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty());
upstream_target_key_from_url(plan.url.as_str(), proxy)
.unwrap_or_else(|| fallback_target_key(plan))
}
pub(crate) fn upstream_target_key_from_url(
upstream_url: &str,
proxy: Option<&str>,
) -> Option<String> {
let parsed = Url::parse(upstream_url).ok();
let Some(url) = parsed else {
return None;
};
let scheme = url.scheme().to_ascii_lowercase();
let Some(host) = url.host_str().map(|host| host.to_ascii_lowercase()) else {
return None;
};
let port = url
.port_or_known_default()
.map(|port| port.to_string())
.unwrap_or_else(|| "-".to_string());
let proxy = proxy
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("-");
format!("{scheme}://{host}:{port}|proxy={proxy}")
Some(format!("{scheme}://{host}:{port}|proxy={proxy}"))
}
fn fallback_target_key(plan: &ExecutionPlan) -> String {
@@ -202,6 +400,18 @@ fn upstream_target_metric_limit() -> usize {
.unwrap_or(DEFAULT_METRIC_TARGET_LIMIT)
}
fn target_queue_budget(fallback: Duration) -> Duration {
std::env::var(TARGET_QUEUE_BUDGET_MS_ENV)
.ok()
.and_then(|value| value.trim().parse::<u64>().ok())
.map(|value| value.clamp(1, MAX_TARGET_QUEUE_BUDGET_MS))
.map(Duration::from_millis)
.unwrap_or_else(|| {
let fallback_ms = u64::try_from(fallback.as_millis()).unwrap_or(u64::MAX);
Duration::from_millis(fallback_ms.clamp(1, DEFAULT_TARGET_QUEUE_BUDGET_MS))
})
}
#[cfg(test)]
mod tests {
use std::time::Duration;
@@ -243,6 +453,37 @@ mod tests {
assert_eq!(upstream_target_key(&left), upstream_target_key(&right));
}
#[test]
fn upstream_target_key_from_url_matches_plan_key_without_proxy() {
let plan = test_plan("http://127.0.0.1:18181/v1/chat/completions?x=1");
assert_eq!(
upstream_target_key_from_url("http://127.0.0.1:18181/v1/responses", None)
.expect("url should parse"),
upstream_target_key(&plan)
);
}
#[test]
fn upstream_target_key_from_url_includes_proxy() {
assert_eq!(
upstream_target_key_from_url(
"https://api.example.com/v1/chat/completions?x=1",
Some("http://proxy.internal:8080")
)
.expect("url should parse"),
"https://api.example.com:443|proxy=http://proxy.internal:8080"
);
}
#[test]
fn target_queue_budget_defaults_to_short_budget() {
assert_eq!(
target_queue_budget(Duration::from_millis(250)),
Duration::from_millis(DEFAULT_TARGET_QUEUE_BUDGET_MS)
);
}
#[tokio::test]
async fn acquire_times_out_when_target_gate_is_saturated() {
let admission = UpstreamTargetAdmission::new(Some(1), Duration::from_millis(1));
@@ -265,5 +506,74 @@ mod tests {
..
}
));
let snapshot = admission
.snapshot_for_plan(&plan)
.expect("target snapshot should exist");
assert_eq!(snapshot.in_flight, 1);
assert_eq!(snapshot.selected_total, 2);
assert_eq!(snapshot.saturated_total, 1);
}
#[test]
fn try_acquire_returns_none_when_target_is_saturated() {
let admission = UpstreamTargetAdmission::new(Some(1), Duration::from_millis(1));
let plan = test_plan("http://127.0.0.1:18181/v1/chat/completions");
let _first = admission
.try_acquire_for_plan(&plan)
.expect("first try acquire should not error")
.expect("first permit should be acquired");
assert!(admission
.try_acquire_for_plan(&plan)
.expect("saturated try acquire should not error")
.is_none());
let snapshot = admission
.snapshot_for_plan(&plan)
.expect("target snapshot should exist");
assert_eq!(snapshot.in_flight, 1);
assert_eq!(snapshot.selected_total, 2);
assert_eq!(snapshot.saturated_total, 1);
let samples = admission.metric_samples();
assert!(samples
.iter()
.any(|sample| sample.name == "upstream_target_selected_total"));
assert!(samples
.iter()
.any(|sample| sample.name == "upstream_target_saturated_total"));
}
#[test]
fn preselect_records_selection_pressure_before_acquire() {
let admission = UpstreamTargetAdmission::new(Some(10), Duration::from_millis(1));
let target = "http://127.0.0.1:18181|proxy=-";
admission.record_preselect_for_target_key(target);
admission.record_preselect_for_target_key(target);
let snapshot = admission
.snapshot_for_target_key(target)
.expect("target snapshot should exist");
assert_eq!(snapshot.in_flight, 0);
assert_eq!(snapshot.raw_seen_total, 0);
assert_eq!(snapshot.preselect_total, 2);
assert_eq!(snapshot.selected_total, 0);
assert_eq!(snapshot.selection_pressure_total, 2);
}
#[test]
fn raw_seen_records_target_without_acquire() {
let admission = UpstreamTargetAdmission::new(Some(10), Duration::from_millis(1));
let target = "http://127.0.0.1:18182|proxy=-";
admission.record_raw_seen_for_target_key(target);
let snapshot = admission
.snapshot_for_target_key(target)
.expect("target snapshot should exist");
assert_eq!(snapshot.in_flight, 0);
assert_eq!(snapshot.raw_seen_total, 1);
assert_eq!(snapshot.preselect_total, 0);
assert_eq!(snapshot.selected_total, 0);
}
}
+1
View File
@@ -5,6 +5,7 @@ pub(crate) mod write;
pub(crate) use aether_usage_runtime::UsageRuntime;
pub use aether_usage_runtime::UsageRuntimeConfig;
pub(crate) use aether_usage_runtime::UsageRuntimeMetricsSnapshot;
pub(crate) use aether_usage_runtime::{
now_ms, UsageEvent, UsageEventData, UsageEventType, UsageQueue, UsageRequestRecordLevel,
USAGE_EVENT_VERSION,
@@ -12,18 +12,43 @@ const DAILY_QUOTA_EPSILON_USD: f64 = 0.000_000_01;
pub(crate) async fn resolve_wallet_auth_gate(
state: &AppState,
auth_snapshot: &GatewayAuthApiKeySnapshot,
) -> Result<Option<WalletAccessDecision>, GatewayError> {
resolve_wallet_auth_gate_with_cache(state, auth_snapshot, true).await
}
pub(crate) async fn resolve_wallet_auth_gate_uncached(
state: &AppState,
auth_snapshot: &GatewayAuthApiKeySnapshot,
) -> Result<Option<WalletAccessDecision>, GatewayError> {
resolve_wallet_auth_gate_with_cache(state, auth_snapshot, false).await
}
async fn resolve_wallet_auth_gate_with_cache(
state: &AppState,
auth_snapshot: &GatewayAuthApiKeySnapshot,
use_cache: bool,
) -> Result<Option<WalletAccessDecision>, GatewayError> {
if !state.has_wallet_data_reader() {
return Ok(None);
}
let wallet = state
.read_wallet_snapshot_for_auth(
&auth_snapshot.user_id,
&auth_snapshot.api_key_id,
auth_snapshot.api_key_is_standalone,
)
.await?;
let wallet = if use_cache {
state
.read_wallet_snapshot_for_auth(
&auth_snapshot.user_id,
&auth_snapshot.api_key_id,
auth_snapshot.api_key_is_standalone,
)
.await?
} else {
state
.read_wallet_snapshot_for_auth_uncached(
&auth_snapshot.user_id,
&auth_snapshot.api_key_id,
auth_snapshot.api_key_is_standalone,
)
.await?
};
let decision = match wallet.as_ref() {
Some(wallet) => map_wallet_snapshot(wallet).access_decision(false),
@@ -1,5 +1,7 @@
mod access;
mod quota;
pub(crate) use access::{local_rejection_from_wallet_access, resolve_wallet_auth_gate};
pub(crate) use access::{
local_rejection_from_wallet_access, resolve_wallet_auth_gate, resolve_wallet_auth_gate_uncached,
};
pub(crate) use quota::spawn_provider_quota_reset_worker;
@@ -971,6 +971,7 @@ mod tests {
&config,
Arc::new(DnsCache::new(Duration::from_secs(60), 16)),
true,
false,
)
.expect("client should build")
}
+51 -23
View File
@@ -38,6 +38,17 @@ pub enum AiStreamExecutionStep {
RemoteDecision,
}
pub const DEFAULT_STREAM_EXECUTION_STEPS: &[AiStreamExecutionStep] = &[
AiStreamExecutionStep::LocalVideoContent,
AiStreamExecutionStep::LocalImage,
AiStreamExecutionStep::LocalOpenAiChat,
AiStreamExecutionStep::LocalOpenAiResponses,
AiStreamExecutionStep::LocalStandardFamily,
AiStreamExecutionStep::LocalSameFormatProvider,
AiStreamExecutionStep::LocalGeminiFiles,
AiStreamExecutionStep::RemoteDecision,
];
#[async_trait]
pub trait AiSyncExecutionPathPort: Send + Sync {
type Response: Send;
@@ -65,6 +76,10 @@ pub trait AiStreamExecutionPathPort: Send + Sync {
fn scheduler_decision_supported(&self) -> bool;
fn stream_execution_steps(&self) -> &'static [AiStreamExecutionStep] {
DEFAULT_STREAM_EXECUTION_STEPS
}
async fn execute_stream_step(
&self,
step: AiStreamExecutionStep,
@@ -135,29 +150,13 @@ where
{
let mut exhausted = None;
if let Some(response) = absorb_stream_step(
port,
AiStreamExecutionStep::LocalVideoContent,
&mut exhausted,
)
.await?
{
return Ok(response);
}
if port.scheduler_decision_supported() {
for step in [
AiStreamExecutionStep::LocalImage,
AiStreamExecutionStep::LocalOpenAiChat,
AiStreamExecutionStep::LocalOpenAiResponses,
AiStreamExecutionStep::LocalStandardFamily,
AiStreamExecutionStep::LocalSameFormatProvider,
AiStreamExecutionStep::LocalGeminiFiles,
AiStreamExecutionStep::RemoteDecision,
] {
if let Some(response) = absorb_stream_step(port, step, &mut exhausted).await? {
return Ok(response);
}
for step in port.stream_execution_steps() {
if *step != AiStreamExecutionStep::LocalVideoContent && !port.scheduler_decision_supported()
{
continue;
}
if let Some(response) = absorb_stream_step(port, *step, &mut exhausted).await? {
return Ok(response);
}
}
@@ -279,6 +278,7 @@ mod tests {
#[derive(Default)]
struct TestStreamPort {
scheduler_supported: bool,
stream_steps: Option<&'static [AiStreamExecutionStep]>,
outcomes: Mutex<VecDeque<AiServingExecutionOutcome<&'static str, &'static str>>>,
calls: Mutex<Vec<String>>,
}
@@ -293,6 +293,11 @@ mod tests {
self.scheduler_supported
}
fn stream_execution_steps(&self) -> &'static [AiStreamExecutionStep] {
self.stream_steps
.unwrap_or(super::DEFAULT_STREAM_EXECUTION_STEPS)
}
async fn execute_stream_step(
&self,
step: AiStreamExecutionStep,
@@ -405,6 +410,7 @@ mod tests {
async fn stream_path_stops_at_first_response() {
let port = TestStreamPort {
scheduler_supported: true,
stream_steps: None,
outcomes: Mutex::new(VecDeque::from([
AiServingExecutionOutcome::NoPath,
AiServingExecutionOutcome::Responded("image_response"),
@@ -424,10 +430,32 @@ mod tests {
);
}
#[tokio::test]
async fn stream_path_runs_preferred_steps_only() {
const CHAT_ONLY: &[AiStreamExecutionStep] = &[AiStreamExecutionStep::LocalOpenAiChat];
let port = TestStreamPort {
scheduler_supported: true,
stream_steps: Some(CHAT_ONLY),
outcomes: Mutex::new(VecDeque::from([AiServingExecutionOutcome::Responded(
"chat_response",
)])),
calls: Mutex::default(),
};
let outcome = run_ai_stream_execution_path(&port).await.unwrap();
assert!(matches!(
outcome,
AiServingExecutionOutcome::Responded("chat_response")
));
assert_eq!(port.calls.lock().unwrap().as_slice(), ["LocalOpenAiChat"]);
}
#[tokio::test]
async fn stream_path_returns_last_exhaustion_without_plan_fallback() {
let port = TestStreamPort {
scheduler_supported: true,
stream_steps: None,
outcomes: Mutex::new(VecDeque::from([
AiServingExecutionOutcome::NoPath,
AiServingExecutionOutcome::Exhausted("local_image_exhausted"),
+1 -1
View File
@@ -113,7 +113,7 @@ pub use dto::{
pub use execution_path::{
run_ai_stream_execution_path, run_ai_sync_execution_path, AiPlanFallbackReason,
AiServingExecutionOutcome, AiStreamExecutionPathPort, AiStreamExecutionStep,
AiSyncExecutionPathPort, AiSyncExecutionStep,
AiSyncExecutionPathPort, AiSyncExecutionStep, DEFAULT_STREAM_EXECUTION_STEPS,
};
pub use failure_diagnostic::{CandidateFailureDiagnostic, CandidateFailureDiagnosticKind};
pub use plan_payload::{
@@ -167,6 +167,8 @@ pub struct RoutingGroupBindingQuery {
#[async_trait]
pub trait RoutingGroupReadRepository: Send + Sync {
fn clear_local_cache(&self) {}
async fn list_routing_groups(&self) -> Result<Vec<StoredRoutingGroup>, crate::DataLayerError>;
async fn find_routing_group(
@@ -172,6 +172,8 @@ pub fn build_passthrough_path_url(
let normalized_base_url =
if trimmed_base_url.ends_with("/v1beta") && trimmed_path.starts_with("/v1beta") {
trimmed_base_url.trim_end_matches("/v1beta")
} else if trimmed_base_url.ends_with("/v1") && trimmed_path.starts_with("/v1/") {
trimmed_base_url.trim_end_matches("/v1")
} else {
trimmed_base_url
};
@@ -681,6 +683,20 @@ mod tests {
);
}
#[test]
fn passthrough_path_does_not_duplicate_openai_v1_root() {
assert_eq!(
build_passthrough_path_url(
"https://api.openai.example/v1?tenant=demo",
"/v1/chat/completions?variant=chat",
Some("trace=1"),
&[]
)
.as_deref(),
Some("https://api.openai.example/v1/chat/completions?tenant=demo&trace=1&variant=chat")
);
}
#[test]
fn merges_base_url_query_for_gemini_files_passthrough_urls() {
assert_eq!(
+26
View File
@@ -438,6 +438,18 @@ impl RuntimeState {
}
}
pub async fn rate_limit_count(&self, key: &str, bucket: u64) -> Result<u32, DataLayerError> {
match self.backend.as_ref() {
RuntimeStateBackend::Memory(memory) => memory.rate_limit_count(key, bucket),
RuntimeStateBackend::Redis(redis) => Ok(redis
.kv
.get(key)
.await?
.and_then(|value| value.parse::<u32>().ok())
.unwrap_or_default()),
}
}
pub async fn set_add(&self, key: &str, member: &str) -> Result<bool, DataLayerError> {
match self.backend.as_ref() {
RuntimeStateBackend::Memory(memory) => Ok(memory.set_add(key, member).await),
@@ -1445,6 +1457,13 @@ mod tests {
.expect("first"),
RateLimitCheck::Allowed { .. }
));
assert_eq!(
runtime
.rate_limit_count(input.user_key, input.bucket)
.await
.expect("count after first"),
1
);
assert_eq!(
runtime
.check_and_consume_rate_limit(input)
@@ -1455,6 +1474,13 @@ mod tests {
limit: 1
}
);
assert_eq!(
runtime
.rate_limit_count(input.user_key, input.bucket)
.await
.expect("count after reject"),
1
);
}
#[tokio::test]
+33 -15
View File
@@ -1,5 +1,6 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex as StdMutex;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
@@ -36,7 +37,7 @@ impl MemoryKvEntry {
pub(crate) struct MemoryRuntimeBackend {
config: MemoryRuntimeStateConfig,
kv: Mutex<HashMap<String, MemoryKvEntry>>,
counters: Mutex<HashMap<String, MemoryCounterEntry>>,
counters: StdMutex<HashMap<String, MemoryCounterEntry>>,
sets: Mutex<HashMap<String, MemorySetEntry>>,
scores: Mutex<HashMap<String, MemoryScoreEntry>>,
queues: Mutex<HashMap<String, MemoryQueueStream>>,
@@ -383,9 +384,12 @@ impl MemoryRuntimeBackend {
key_limit: u32,
ttl: Duration,
) -> Result<crate::RateLimitCheck, crate::DataLayerError> {
let mut counters = self.counters.lock().await;
let mut counters = self.counters.lock().map_err(|_| {
DataLayerError::UnexpectedValue("memory rate-limit counter lock poisoned".to_string())
})?;
let now = Instant::now();
counters.retain(|_, entry| entry.expires_at > now && entry.bucket >= bucket);
prune_rate_limit_counter(&mut counters, user_key, bucket, now);
prune_rate_limit_counter(&mut counters, key_key, bucket, now);
if user_limit > 0 {
let user_count = counters
@@ -416,8 +420,6 @@ impl MemoryRuntimeBackend {
}
let mut remaining = None::<u32>;
let mut user_next = None::<u32>;
let mut key_next = None::<u32>;
let expires_at = now + ttl;
if user_limit > 0 {
let next = counters
@@ -434,7 +436,6 @@ impl MemoryRuntimeBackend {
})
.value;
remaining = Some(user_limit.saturating_sub(next));
user_next = Some(next);
}
if key_limit > 0 {
let next = counters
@@ -452,21 +453,24 @@ impl MemoryRuntimeBackend {
.value;
let key_remaining = key_limit.saturating_sub(next);
remaining = Some(remaining.map_or(key_remaining, |value| value.min(key_remaining)));
key_next = Some(next);
}
drop(counters);
if let Some(next) = user_next {
self.kv_set(user_key, next.to_string(), Some(ttl)).await;
}
if let Some(next) = key_next {
self.kv_set(key_key, next.to_string(), Some(ttl)).await;
}
Ok(crate::RateLimitCheck::Allowed {
remaining: remaining.unwrap_or(0),
})
}
pub(crate) fn rate_limit_count(&self, key: &str, bucket: u64) -> Result<u32, DataLayerError> {
let mut counters = self.counters.lock().map_err(|_| {
DataLayerError::UnexpectedValue("memory rate-limit counter lock poisoned".to_string())
})?;
prune_rate_limit_counter(&mut counters, key, bucket, Instant::now());
Ok(counters
.get(key)
.filter(|entry| entry.bucket == bucket)
.map(|entry| entry.value)
.unwrap_or_default())
}
pub(crate) async fn set_add(&self, key: &str, member: &str) -> bool {
let mut sets = self.sets.lock().await;
prune_memory_key(&mut sets, key, Instant::now());
@@ -913,6 +917,20 @@ fn prune_kv(kv: &mut HashMap<String, MemoryKvEntry>, now: Instant) {
kv.retain(|_, entry| !entry.is_expired(now));
}
fn prune_rate_limit_counter(
counters: &mut HashMap<String, MemoryCounterEntry>,
key: &str,
bucket: u64,
now: Instant,
) {
if counters
.get(key)
.is_some_and(|entry| entry.expires_at <= now || entry.bucket < bucket)
{
counters.remove(key);
}
}
fn prune_memory_key<T>(values: &mut HashMap<String, T>, key: &str, now: Instant)
where
T: MemoryExpiringKey,
+1 -1
View File
@@ -11,7 +11,7 @@ async-stream.workspace = true
aether-data.workspace = true
aether-data-contracts.workspace = true
aether-contracts.workspace = true
aether-gateway.workspace = true
aether-gateway = { workspace = true, features = ["testkit"] }
aether-http.workspace = true
aether-runtime.workspace = true
aether-runtime-state.workspace = true
@@ -331,6 +331,7 @@ async fn run_tunnel_curve(
concurrency: relay_concurrency,
timeout: config.timeout,
response_mode: HttpLoadProbeResponseMode::FullBody,
..HttpLoadProbeConfig::default()
};
let started_at = Instant::now();
let result = run_http_load_probe(&probe)
@@ -480,6 +481,7 @@ fn execution_probe_config(
concurrency,
timeout,
response_mode: HttpLoadProbeResponseMode::FullBody,
..HttpLoadProbeConfig::default()
}
}
@@ -506,6 +508,7 @@ fn chat_probe_config(
concurrency,
timeout,
response_mode: HttpLoadProbeResponseMode::FullBody,
..HttpLoadProbeConfig::default()
}
}

Some files were not shown because too many files have changed in this diff Show More