mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-08 20:20:19 +08:00
Improve gateway transport and usage runtime
This commit is contained in:
Generated
+5
@@ -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",
|
||||
]
|
||||
|
||||
@@ -30,6 +30,9 @@ members = [
|
||||
"crates/aether-runtime",
|
||||
"crates/aether-testkit",
|
||||
]
|
||||
default-members = [
|
||||
"apps/aether-gateway",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -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
|
||||
|
||||
# ==================== 最小运行时打包 ====================
|
||||
|
||||
@@ -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
|
||||
|
||||
# ==================== 最小运行时打包 ====================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+42
-2
@@ -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
@@ -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");
|
||||
|
||||
Vendored
+5
-2
@@ -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;
|
||||
|
||||
@@ -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("a_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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user