feat(codex): 按操作语义路由 Responses V2 压缩

(cherry picked from commit 2fc604e047)
This commit is contained in:
MMEXA
2026-07-16 00:34:42 +08:00
committed by elky
parent 598b2fb374
commit 20b27a13b2
75 changed files with 732 additions and 50 deletions
@@ -706,6 +706,7 @@ pub(crate) async fn build_lazy_requested_model_execution_candidate_attempt_sourc
trace_id: &str,
client_api_format: &str,
requested_model: &str,
request_operation: Option<&str>,
require_streaming: bool,
auth_snapshot: &GatewayAuthApiKeySnapshot,
client_session_affinity: Option<&ClientSessionAffinity>,
@@ -734,6 +735,7 @@ where
model_directive_policy,
client_api_format,
requested_model,
request_operation,
require_streaming,
required_capabilities,
auth_snapshot,
@@ -1141,6 +1143,7 @@ async fn resolve_priority_candidate_page_with_cache(
let key = CandidateResolvedPageCacheKey::new(
&cursor.requested_model,
cursor.page_cursor.resolved_page_cache_request_operation(),
&cursor.client_api_format,
true,
&cursor.auth_snapshot,
@@ -2190,6 +2193,7 @@ mod tests {
&model_directive_policy,
"openai:chat",
"gpt-5",
None,
true,
None,
&auth_snapshot,
@@ -2245,6 +2249,7 @@ mod tests {
&model_directive_policy,
"openai:chat",
"gpt-5",
None,
true,
None,
&auth_snapshot,
@@ -2282,6 +2287,7 @@ mod tests {
&model_directive_policy,
"openai:chat",
"gpt-5",
None,
true,
None,
&auth_snapshot,
@@ -6,9 +6,10 @@ use aether_routing_core::ResolvedRoutingPolicy;
use aether_runtime::ConcurrencyPermit;
use aether_scheduler_core::{
enumerate_minimal_candidate_selection_with_model_directives, normalize_api_format,
resolve_requested_global_model_name_with_model_directives,
row_supports_requested_model_with_model_directives, ClientSessionAffinity,
EnumerateMinimalCandidateSelectionInput, SchedulerMinimalCandidateSelectionCandidate,
resolve_requested_global_model_name_with_model_directives_and_request_operation,
row_supports_requested_model_with_model_directives_and_request_operation,
ClientSessionAffinity, EnumerateMinimalCandidateSelectionInput,
SchedulerMinimalCandidateSelectionCandidate,
};
use async_trait::async_trait;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
@@ -56,6 +57,7 @@ struct GatewayLocalCandidatePreselectionPort<'a> {
state: PlannerAppState<'a>,
client_api_format: &'a str,
requested_model: &'a str,
request_operation: Option<&'a str>,
require_streaming: bool,
required_capabilities: Option<&'a serde_json::Value>,
auth_snapshot: &'a GatewayAuthApiKeySnapshot,
@@ -112,7 +114,7 @@ impl AiCandidatePreselectionPort for GatewayLocalCandidatePreselectionPort<'_> {
let auth_snapshot = matches_client_format.then_some(self.auth_snapshot);
let (candidates, skipped_candidates) = self
.state
.list_selectable_candidates_with_skip_reasons(
.list_selectable_candidates_with_skip_reasons_for_request_operation(
candidate_api_format,
self.routing_model(candidate_api_format),
self.require_streaming,
@@ -121,6 +123,7 @@ impl AiCandidatePreselectionPort for GatewayLocalCandidatePreselectionPort<'_> {
self.client_session_affinity,
self.ranking_seed,
false,
self.request_operation,
)
.await?;
@@ -197,6 +200,7 @@ pub(crate) async fn preselect_local_execution_candidates_with_serving(
model_directive_policy: &crate::system_features::ModelDirectivePolicySnapshot,
client_api_format: &str,
requested_model: &str,
request_operation: Option<&str>,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: &GatewayAuthApiKeySnapshot,
@@ -221,6 +225,7 @@ pub(crate) async fn preselect_local_execution_candidates_with_serving(
model_directive_policy,
client_api_format,
requested_model,
request_operation,
require_streaming,
required_capabilities,
auth_snapshot,
@@ -239,6 +244,7 @@ pub(crate) async fn preselect_local_execution_candidates_for_api_formats_with_se
model_directive_policy: &crate::system_features::ModelDirectivePolicySnapshot,
client_api_format: &str,
requested_model: &str,
request_operation: Option<&str>,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: &GatewayAuthApiKeySnapshot,
@@ -263,6 +269,7 @@ pub(crate) async fn preselect_local_execution_candidates_for_api_formats_with_se
state,
client_api_format,
requested_model,
request_operation,
require_streaming,
required_capabilities,
auth_snapshot,
@@ -283,6 +290,7 @@ pub(crate) struct LocalCandidatePreselectionPageCursor<'a> {
trace_id: String,
client_api_format: String,
requested_model: String,
request_operation: Option<String>,
require_streaming: bool,
required_capabilities: Option<serde_json::Value>,
auth_snapshot: GatewayAuthApiKeySnapshot,
@@ -336,6 +344,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
model_directive_policy: &crate::system_features::ModelDirectivePolicySnapshot,
client_api_format: &str,
requested_model: &str,
request_operation: Option<&str>,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: &GatewayAuthApiKeySnapshot,
@@ -370,6 +379,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
trace_id: trace_id.unwrap_or_default().to_string(),
client_api_format: client_api_format.to_string(),
requested_model: requested_model.to_string(),
request_operation: request_operation.map(str::to_string),
require_streaming,
required_capabilities: required_capabilities.cloned(),
auth_snapshot: auth_snapshot.clone(),
@@ -452,6 +462,10 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
self.key_mode.cache_key_name()
}
pub(crate) fn resolved_page_cache_request_operation(&self) -> Option<&str> {
self.request_operation.as_deref()
}
pub(crate) fn resolved_page_cache_use_api_format_alias_match(&self) -> bool {
self.use_api_format_alias_match
}
@@ -515,6 +529,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
> {
let key = CandidatePageCacheKey::new(
&self.requested_model,
self.request_operation.as_deref(),
&self.client_api_format,
self.require_streaming,
&self.auth_snapshot,
@@ -965,11 +980,12 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
.map_err(|err| GatewayError::Internal(err.to_string()))?
.into_iter()
.filter(|row| {
row_supports_requested_model_with_model_directives(
row_supports_requested_model_with_model_directives_and_request_operation(
row,
&routing_model,
normalized_api_format,
false,
self.request_operation.as_deref(),
)
})
.collect::<Vec<_>>();
@@ -1009,12 +1025,15 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
if let Some(value) = self.resolved_global_model_names.get(normalized_api_format) {
value.clone()
} else {
let Some(value) = resolve_requested_global_model_name_with_model_directives(
&rows,
&routing_model,
normalized_api_format,
false,
) else {
let Some(value) =
resolve_requested_global_model_name_with_model_directives_and_request_operation(
&rows,
&routing_model,
normalized_api_format,
false,
self.request_operation.as_deref(),
)
else {
return Ok(None);
};
self.resolved_global_model_names
@@ -1037,6 +1056,7 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
EnumerateMinimalCandidateSelectionInput {
rows,
normalized_api_format,
request_operation: self.request_operation.as_deref(),
requested_model_name: &routing_model,
resolved_global_model_name: resolved_global_model_name.as_str(),
require_streaming: self.require_streaming,
@@ -1316,6 +1336,7 @@ mod tests {
&model_directive_policy,
"openai:chat",
"gpt-5",
None,
true,
None,
&auth_snapshot,
@@ -1531,6 +1552,7 @@ mod tests {
priority: 1,
api_formats: None,
endpoint_ids: Some(vec!["endpoint-opg-openai".to_string()]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1561,6 +1583,7 @@ mod tests {
&model_directive_policy,
"claude:messages",
"gpt-5.5-xhigh",
None,
false,
None,
&auth_snapshot,
@@ -1590,6 +1613,70 @@ mod tests {
);
}
#[tokio::test]
async fn paged_preselection_prefers_operation_scoped_mapping_for_compaction() {
let mut row = openai_responses_mapping_row();
row.global_model_mappings = None;
row.global_model_name = "gpt-5.6-sol".to_string();
row.model_provider_model_name = "gpt-5.6-sol".to_string();
row.model_provider_model_mappings = Some(vec![
StoredProviderModelMapping {
name: "gpt-5.6-sol".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
},
StoredProviderModelMapping {
name: "gpt-5.6-terra".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: Some(vec!["compact".to_string()]),
},
]);
let repository: Arc<dyn MinimalCandidateSelectionReadRepository> =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed([row]));
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 model_directive_policy =
crate::system_features::ModelDirectivePolicySnapshot::load(&app).await;
let mut cursor = LocalCandidatePreselectionPageCursor::new(
PlannerAppState::new(&app),
&model_directive_policy,
"openai:responses",
"gpt-5.6-sol",
Some("compact"),
false,
None,
&auth_snapshot,
None,
None,
None,
true,
LocalCandidatePreselectionKeyMode::ProviderEndpointKeyModelAndApiFormat,
true,
None,
)
.await;
let page = cursor
.next_page()
.await
.expect("preselection should succeed")
.expect("compact mapping should find a provider");
assert_eq!(page.candidates.len(), 1);
assert_eq!(
page.candidates[0].selected_provider_model_name,
"gpt-5.6-terra"
);
}
#[tokio::test]
async fn custom_policy_suffix_uses_the_same_base_model_for_candidate_selection() {
let mut row = openai_responses_mapping_row();
@@ -1634,6 +1721,7 @@ mod tests {
&model_directive_policy,
"openai:responses",
"deployment-alias-VendorFuture",
None,
false,
None,
&auth_snapshot,
@@ -1695,6 +1783,7 @@ mod tests {
&model_directive_policy,
"claude:messages",
"deepseek-v4-pro",
None,
false,
None,
&auth_snapshot,
@@ -1773,6 +1862,7 @@ mod tests {
&model_directive_policy,
"claude:messages",
"gpt-5",
None,
false,
None,
&auth_snapshot,
@@ -124,6 +124,7 @@ pub(super) async fn materialize_local_standard_candidate_attempts(
&input.model_directive_policy,
spec_metadata.api_format,
&input.requested_model,
None,
false,
input.required_capabilities.as_ref(),
&input.auth_snapshot,
@@ -250,6 +251,7 @@ pub(super) async fn build_local_standard_candidate_attempt_source<'a>(
trace_id,
spec_metadata.api_format,
&input.requested_model,
None,
spec_metadata.require_streaming,
&input.auth_snapshot,
input.client_session_affinity.as_ref(),
@@ -345,6 +347,7 @@ async fn maybe_append_gemini_image_openai_image_preselection(
&input.model_directive_policy,
spec_metadata.api_format,
&input.requested_model,
None,
spec_metadata.require_streaming,
input.required_capabilities.as_ref(),
&input.auth_snapshot,
@@ -297,6 +297,7 @@ pub(crate) async fn build_lazy_local_openai_chat_candidate_attempt_source<'a>(
trace_id,
"openai:chat",
&input.requested_model,
None,
require_streaming,
&input.auth_snapshot,
input.client_session_affinity.as_ref(),
@@ -24,6 +24,7 @@ pub(crate) async fn list_local_openai_chat_candidates(
&input.model_directive_policy,
"openai:chat",
&input.requested_model,
None,
require_streaming,
input.required_capabilities.as_ref(),
&input.auth_snapshot,
@@ -31,8 +31,8 @@ use crate::ai_serving::planner::spec_metadata::local_openai_responses_spec_metad
use crate::ai_serving::planner::CandidateFailureDiagnostic;
use crate::ai_serving::{
ai_local_execution_contract_for_formats, extract_pool_sticky_session_token,
resolve_local_decision_execution_runtime_auth_context, ExecutionRuntimeAuthContext,
GatewayControlDecision, PlannerAppState,
openai_responses_request_operation, resolve_local_decision_execution_runtime_auth_context,
ExecutionRuntimeAuthContext, GatewayControlDecision, PlannerAppState,
};
use crate::client_session_affinity::client_session_affinity_from_parts;
use crate::{AppState, GatewayError};
@@ -163,6 +163,7 @@ pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
spec: LocalOpenAiResponsesSpec,
) -> Result<(Vec<LocalOpenAiResponsesCandidateAttempt>, usize), GatewayError> {
let spec_metadata = local_openai_responses_spec_metadata(spec);
let request_operation = openai_responses_request_operation(spec_metadata.api_format, body_json);
let planner_state = PlannerAppState::new(state);
let sticky_session_token = extract_pool_sticky_session_token(body_json);
let auth_context: &ExecutionRuntimeAuthContext = &input.auth_context;
@@ -176,6 +177,7 @@ pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
&input.model_directive_policy,
spec_metadata.api_format,
&input.requested_model,
request_operation,
spec_metadata.require_streaming,
input.required_capabilities.as_ref(),
&input.auth_snapshot,
@@ -262,6 +264,7 @@ pub(crate) async fn build_local_openai_responses_candidate_attempt_source<'a>(
spec: LocalOpenAiResponsesSpec,
) -> Result<(LocalOpenAiResponsesCandidateAttemptSource<'a>, usize), GatewayError> {
let spec_metadata = local_openai_responses_spec_metadata(spec);
let request_operation = openai_responses_request_operation(spec_metadata.api_format, body_json);
let planner_state = PlannerAppState::new(state);
let sticky_session_token = extract_pool_sticky_session_token(body_json);
let auth_context: &ExecutionRuntimeAuthContext = &input.auth_context;
@@ -287,6 +290,7 @@ pub(crate) async fn build_local_openai_responses_candidate_attempt_source<'a>(
trace_id,
spec_metadata.api_format,
&input.requested_model,
request_operation,
spec_metadata.require_streaming,
&input.auth_snapshot,
input.client_session_affinity.as_ref(),
@@ -372,6 +376,7 @@ pub(crate) async fn build_local_openai_responses_image_candidate_attempt_source<
&input.model_directive_policy,
spec_metadata.api_format,
&input.requested_model,
None,
false,
input.required_capabilities.as_ref(),
&input.auth_snapshot,
@@ -53,13 +53,45 @@ impl<'a> PlannerAppState<'a> {
Vec<SchedulerSkippedCandidate>,
),
GatewayError,
> {
self.list_selectable_candidates_with_skip_reasons_for_request_operation(
api_format,
global_model_name,
require_streaming,
required_capabilities,
auth_snapshot,
client_session_affinity,
now_unix_secs,
enable_model_directives,
None,
)
.await
}
pub(crate) async fn list_selectable_candidates_with_skip_reasons_for_request_operation(
self,
api_format: &str,
global_model_name: &str,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
client_session_affinity: Option<&ClientSessionAffinity>,
now_unix_secs: u64,
enable_model_directives: bool,
request_operation: Option<&str>,
) -> Result<
(
Vec<SchedulerMinimalCandidateSelectionCandidate>,
Vec<SchedulerSkippedCandidate>,
),
GatewayError,
> {
let wait_timeout = Duration::from_millis(API_KEY_CONCURRENCY_WAIT_TIMEOUT_MS);
let wait_interval = Duration::from_millis(API_KEY_CONCURRENCY_WAIT_POLL_INTERVAL_MS.max(1));
let wait_deadline = Instant::now() + wait_timeout;
let mut attempt_now_unix_secs = now_unix_secs;
loop {
let result = crate::scheduler::candidate::list_selectable_candidates_with_skip_reasons(
let result = crate::scheduler::candidate::list_selectable_candidates_with_skip_reasons_for_request_operation(
self.app().data.as_ref(),
self.app(),
api_format,
@@ -70,6 +102,7 @@ impl<'a> PlannerAppState<'a> {
client_session_affinity,
attempt_now_unix_secs,
enable_model_directives,
request_operation,
)
.await?;
@@ -165,5 +165,5 @@ pub(crate) use aether_ai_formats::api::{
pub(crate) use aether_ai_formats::{
api_format_defaults_to_client_error_failover, api_format_defaults_to_non_stream,
api_format_permission_covers, intersect_api_format_allowed_lists, is_embedding_api_format,
is_rerank_api_format,
is_rerank_api_format, openai_responses_request_operation,
};
+31
View File
@@ -82,6 +82,7 @@ pub(crate) struct CandidateRowPageCacheKey {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CandidatePageCacheKey {
requested_model: String,
request_operation: String,
client_api_format: String,
auth_identity: CandidatePageAuthIdentity,
require_streaming: bool,
@@ -131,6 +132,7 @@ impl CandidatePageCacheKey {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
requested_model: &str,
request_operation: Option<&str>,
client_api_format: &str,
require_streaming: bool,
auth_snapshot: &GatewayAuthApiKeySnapshot,
@@ -145,6 +147,7 @@ impl CandidatePageCacheKey {
) -> Self {
Self {
requested_model: normalize_text_key(requested_model),
request_operation: normalize_text_key(request_operation.unwrap_or_default()),
client_api_format: normalize_api_format(client_api_format),
auth_identity: CandidatePageAuthIdentity::from_auth_snapshot(auth_snapshot),
require_streaming,
@@ -164,6 +167,7 @@ impl CandidateResolvedPageCacheKey {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
requested_model: &str,
request_operation: Option<&str>,
client_api_format: &str,
require_streaming: bool,
auth_snapshot: &GatewayAuthApiKeySnapshot,
@@ -180,6 +184,7 @@ impl CandidateResolvedPageCacheKey {
Self {
page_key: CandidatePageCacheKey::new(
requested_model,
request_operation,
client_api_format,
require_streaming,
auth_snapshot,
@@ -564,6 +569,7 @@ mod tests {
let auth_b = auth_snapshot("user-b", "key-a");
let base = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -578,6 +584,7 @@ mod tests {
);
let different_user = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_b,
@@ -592,6 +599,22 @@ mod tests {
);
let different_model = CandidatePageCacheKey::new(
"gpt-4.1",
None,
"openai:chat",
true,
&auth_a,
Some(&json!({"vision": true})),
None,
Some("bearer"),
7,
"provider_endpoint_key_model",
true,
None,
"policy-a",
);
let different_operation = CandidatePageCacheKey::new(
"gpt-4o",
Some("compact"),
"openai:chat",
true,
&auth_a,
@@ -606,6 +629,7 @@ mod tests {
);
let different_format = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:responses",
true,
&auth_a,
@@ -620,6 +644,7 @@ mod tests {
);
let different_capabilities = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -634,6 +659,7 @@ mod tests {
);
let same_policy = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -648,6 +674,7 @@ mod tests {
);
let different_policy = CandidatePageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -664,12 +691,14 @@ mod tests {
assert_eq!(base, same_policy);
assert_ne!(base, different_user);
assert_ne!(base, different_model);
assert_ne!(base, different_operation);
assert_ne!(base, different_format);
assert_ne!(base, different_capabilities);
assert_ne!(base, different_policy);
let resolved_base = CandidateResolvedPageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -685,6 +714,7 @@ mod tests {
);
let resolved_same_policy = CandidateResolvedPageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -700,6 +730,7 @@ mod tests {
);
let resolved_different_policy = CandidateResolvedPageCacheKey::new(
"gpt-4o",
None,
"openai:chat",
true,
&auth_a,
@@ -621,6 +621,7 @@ mod tests {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -954,6 +955,7 @@ mod tests {
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let state = state_with_rows(vec![row]);
let decision = decision_with_allowed_models(vec!["gpt-5".to_string()]);
@@ -239,6 +239,29 @@ pub(crate) async fn enumerate_minimal_candidate_selection_with_required_capabili
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
enable_model_directives: bool,
) -> Result<Vec<SchedulerMinimalCandidateSelectionCandidate>, DataLayerError> {
enumerate_minimal_candidate_selection_with_required_capabilities_for_request_operation(
state,
api_format,
requested_model_name,
require_streaming,
auth_snapshot,
required_capabilities,
enable_model_directives,
None,
)
.await
}
pub(crate) async fn enumerate_minimal_candidate_selection_with_required_capabilities_for_request_operation(
state: &(impl MinimalCandidateSelectionRowSource + Sync),
api_format: &str,
requested_model_name: &str,
require_streaming: bool,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
required_capabilities: Option<&serde_json::Value>,
enable_model_directives: bool,
request_operation: Option<&str>,
) -> Result<Vec<SchedulerMinimalCandidateSelectionCandidate>, DataLayerError> {
let normalized_api_format = normalize_api_format(api_format);
if normalized_api_format.is_empty() {
@@ -267,6 +290,7 @@ pub(crate) async fn enumerate_minimal_candidate_selection_with_required_capabili
EnumerateMinimalCandidateSelectionInput {
rows,
normalized_api_format: &normalized_api_format,
request_operation,
requested_model_name,
resolved_global_model_name: resolved_global_model_name.as_str(),
require_streaming,
+2
View File
@@ -590,6 +590,7 @@ fn sample_minimal_candidate_selection_row(
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: None,
model_is_active: true,
@@ -907,6 +908,7 @@ async fn data_state_reads_minimal_candidate_selection_with_auth_filters() {
enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
rows,
normalized_api_format: "openai:chat",
request_operation: None,
requested_model_name: "gpt-4.1",
resolved_global_model_name: "gpt-4.1",
require_streaming: false,
@@ -213,6 +213,7 @@ fn provider_query_parse_embedded_provider_model_mappings(
priority: 1,
api_formats: None,
endpoint_ids: None,
operations: None,
}]))
}
@@ -237,6 +238,7 @@ fn provider_query_parse_provider_model_mappings_array(
priority: 1,
api_formats: None,
endpoint_ids: None,
operations: None,
});
}
}
@@ -293,6 +295,11 @@ fn provider_query_parse_provider_model_mapping_object_lenient(
object.get("endpoint_ids"),
"models.provider_model_mappings.endpoint_ids",
)?;
let operations = provider_query_parse_mapping_string_list(
object.get("operations"),
"models.provider_model_mappings.operations",
)?
.and_then(provider_query_normalize_request_operations);
Ok(Some(StoredProviderModelMapping {
name: name.to_string(),
@@ -303,9 +310,19 @@ fn provider_query_parse_provider_model_mapping_object_lenient(
})?,
api_formats,
endpoint_ids,
operations,
}))
}
fn provider_query_normalize_request_operations(values: Vec<String>) -> Option<Vec<String>> {
let operations = values
.into_iter()
.map(|value| value.trim().to_ascii_lowercase())
.filter(|value| !value.is_empty())
.collect::<Vec<_>>();
(!operations.is_empty()).then_some(operations)
}
fn provider_query_parse_mapping_string_list(
value: Option<&Value>,
field_name: &str,
@@ -389,6 +406,7 @@ mod tests {
priority: 1,
api_formats: Some(vec![api_format.to_string()]),
endpoint_ids: None,
operations: None,
}
}
@@ -15,7 +15,7 @@ use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use uuid::Uuid;
fn normalize_provider_model_mappings_api_formats(
fn normalize_provider_model_mapping_scopes(
value: Option<serde_json::Value>,
) -> Option<serde_json::Value> {
let Some(mut value) = value else {
@@ -36,6 +36,9 @@ fn normalize_provider_model_mappings_api_formats(
normalize_provider_model_mapping_string_array_field(object, "endpoint_ids", |value| {
value.trim().to_string()
});
normalize_provider_model_mapping_string_array_field(object, "operations", |value| {
value.trim().to_ascii_lowercase()
});
}
Some(value)
}
@@ -155,7 +158,7 @@ impl<'a> AdminAppState<'a> {
"price_per_request",
)?;
let tiered_pricing = normalize_json_object(payload.tiered_pricing, "tiered_pricing")?;
let provider_model_mappings = normalize_provider_model_mappings_api_formats(
let provider_model_mappings = normalize_provider_model_mapping_scopes(
normalize_json_array(payload.provider_model_mappings, "provider_model_mappings")?,
);
let config = normalize_json_object(payload.config, "config")?;
@@ -241,7 +244,7 @@ impl<'a> AdminAppState<'a> {
existing.tiered_pricing.clone()
};
let provider_model_mappings = if fields.contains("provider_model_mappings") {
normalize_provider_model_mappings_api_formats(normalize_json_array(
normalize_provider_model_mapping_scopes(normalize_json_array(
payload.provider_model_mappings,
"provider_model_mappings",
)?)
@@ -488,6 +488,7 @@ fn build_users_me_usage_record_payload(
"cache_read_input_tokens": item.cache_read_input_tokens,
"status_code": item.status_code,
"error_message": item.error_message,
"request_type": item.request_type,
"input_price_per_1m": input_price_per_1m,
"output_price_per_1m": output_price_per_1m,
"cache_creation_price_per_1m": cache_creation_price_per_1m,
@@ -525,6 +526,7 @@ fn build_users_me_usage_active_payload(item: &StoredRequestUsageAudit) -> serde_
let mut payload = json!({
"id": item.id,
"status": item.status,
"request_type": item.request_type,
"input_tokens": item.input_tokens,
"effective_input_tokens": users_me_usage_effective_input_tokens(item),
"output_tokens": item.output_tokens,
@@ -2,7 +2,7 @@ use aether_scheduler_core::SchedulerMinimalCandidateSelectionCandidate;
use crate::data::auth::GatewayAuthApiKeySnapshot;
use crate::data::candidate_selection::{
enumerate_minimal_candidate_selection_with_required_capabilities,
enumerate_minimal_candidate_selection_with_required_capabilities_for_request_operation,
MinimalCandidateSelectionRowSource,
};
use crate::GatewayError;
@@ -15,8 +15,9 @@ pub(super) async fn enumerate_scheduler_candidates(
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
enable_model_directives: bool,
request_operation: Option<&str>,
) -> Result<Vec<SchedulerMinimalCandidateSelectionCandidate>, GatewayError> {
enumerate_minimal_candidate_selection_with_required_capabilities(
enumerate_minimal_candidate_selection_with_required_capabilities_for_request_operation(
selection_row_source,
api_format,
global_model_name,
@@ -24,6 +25,7 @@ pub(super) async fn enumerate_scheduler_candidates(
auth_snapshot,
required_capabilities,
enable_model_directives,
request_operation,
)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
@@ -116,6 +116,42 @@ pub(crate) async fn list_selectable_candidates_with_skip_reasons(
client_session_affinity,
now_unix_secs,
enable_model_directives,
None,
)
.await
}
pub(crate) async fn list_selectable_candidates_with_skip_reasons_for_request_operation(
selection_row_source: &(impl MinimalCandidateSelectionRowSource + Sync),
runtime_state: &impl SchedulerRuntimeState,
api_format: &str,
global_model_name: &str,
require_streaming: bool,
required_capabilities: Option<&serde_json::Value>,
auth_snapshot: Option<&GatewayAuthApiKeySnapshot>,
client_session_affinity: Option<&ClientSessionAffinity>,
now_unix_secs: u64,
enable_model_directives: bool,
request_operation: Option<&str>,
) -> Result<
(
Vec<SchedulerMinimalCandidateSelectionCandidate>,
Vec<SchedulerSkippedCandidate>,
),
GatewayError,
> {
collect_selectable_candidates_with_skip_reasons(
selection_row_source,
runtime_state,
api_format,
global_model_name,
require_streaming,
required_capabilities,
auth_snapshot,
client_session_affinity,
now_unix_secs,
enable_model_directives,
request_operation,
)
.await
}
@@ -237,6 +273,7 @@ pub(crate) async fn list_selectable_candidates_for_required_capability_without_r
client_session_affinity,
now_unix_secs,
false,
None,
)
.await?;
all_attempts_blocked_by_auth_limit &=
@@ -81,6 +81,7 @@ pub(super) async fn select_minimal_candidate(
required_capabilities,
auth_snapshot,
enable_model_directives,
None,
)
.await?;
let selected = collect_selectable_enumerated_candidates_with_skip_reasons(
@@ -137,6 +138,7 @@ pub(super) async fn collect_selectable_candidates(
client_session_affinity,
now_unix_secs,
enable_model_directives,
None,
)
.await?
.0)
@@ -153,6 +155,7 @@ pub(super) async fn collect_selectable_candidates_with_skip_reasons(
client_session_affinity: Option<&ClientSessionAffinity>,
now_unix_secs: u64,
enable_model_directives: bool,
request_operation: Option<&str>,
) -> Result<
(
Vec<SchedulerMinimalCandidateSelectionCandidate>,
@@ -174,6 +177,7 @@ pub(super) async fn collect_selectable_candidates_with_skip_reasons(
required_capabilities,
auth_snapshot,
enable_model_directives,
request_operation,
)
.await?;
collect_selectable_enumerated_candidates_with_skip_reasons(
@@ -107,6 +107,7 @@ async fn same_priority_candidates_are_distributed_by_affinity_key() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let mut second = sample_row();
@@ -124,6 +125,7 @@ async fn same_priority_candidates_are_distributed_by_affinity_key() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -142,6 +144,7 @@ async fn same_priority_candidates_are_distributed_by_affinity_key() {
enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
rows,
normalized_api_format: "openai:chat",
request_operation: None,
requested_model_name: "gpt-4.1",
resolved_global_model_name: "gpt-4.1",
require_streaming: false,
@@ -45,6 +45,7 @@ fn provider_model_mapping_respects_endpoint_scope() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-selected".to_string()]),
operations: None,
}]);
assert_eq!(
@@ -101,6 +102,7 @@ fn resolves_requested_global_model_from_provider_model_alias() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let resolved = resolve_requested_global_model_name(&[row], "gpt-5.2", "openai:chat");
@@ -156,6 +158,7 @@ async fn enumerate_minimal_candidate_selection_resolves_provider_model_alias() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -193,6 +196,7 @@ async fn enumerate_minimal_candidate_selection_filters_endpoint_scoped_alias_row
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-selected".to_string()]),
operations: None,
}]);
let mut other = selected.clone();
@@ -248,6 +252,7 @@ async fn enumerate_minimal_candidate_selection_keeps_only_resolved_global_model_
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -287,6 +292,7 @@ async fn enumerate_minimal_candidate_selection_allows_resolved_global_model_in_a
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidates = Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -105,6 +105,7 @@ async fn collect_selectable_candidates_with_skip_reasons(
None,
now_unix_secs,
false,
None,
)
.await
}
@@ -843,6 +844,7 @@ async fn selects_next_candidate_when_first_provider_quota_is_exhausted() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
first.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 1}));
@@ -858,6 +860,7 @@ async fn selects_next_candidate_when_first_provider_quota_is_exhausted() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
second.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 2}));
@@ -914,6 +917,7 @@ async fn cooled_down_when_recent_failures_are_recorded_for_same_key() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
first.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 1}));
@@ -929,6 +933,7 @@ async fn cooled_down_when_recent_failures_are_recorded_for_same_key() {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
second.key_global_priority_by_format = Some(serde_json::json!({"openai:chat": 2}));
@@ -40,12 +40,14 @@ pub(super) fn sample_row() -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
},
StoredProviderModelMapping {
name: "gpt-4.1-responses".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
},
]),
model_supports_streaming: None,
+1
View File
@@ -1404,6 +1404,7 @@ mod tests {
let ttl = Duration::from_secs(300);
let cache_key = CandidatePageCacheKey::new(
"gpt-5",
None,
"openai:chat",
true,
&sample_auth_snapshot(),
+1
View File
@@ -247,6 +247,7 @@ fn openai_chat_pressure_candidates(
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec![pressure_endpoint_id(index)]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -138,6 +138,7 @@ async fn gateway_executes_openai_chat_sync_upstream_stream_via_local_finalize_re
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -637,6 +638,7 @@ async fn gateway_executes_openai_chat_cross_format_upstream_stream_via_local_fin
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1082,6 +1084,7 @@ async fn gateway_executes_openai_chat_cross_format_tool_use_upstream_stream_via_
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1564,6 +1567,7 @@ async fn gateway_executes_openai_chat_antigravity_cross_format_sync_via_local_fi
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2074,6 +2078,7 @@ async fn gateway_executes_openai_chat_cross_format_claude_upstream_sync_via_loca
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2433,6 +2438,7 @@ async fn gateway_executes_openai_chat_cross_format_gemini_upstream_sync_via_loca
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -113,6 +113,7 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream
priority: 1,
api_formats: Some(vec!["openai:responses:compact".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -113,6 +113,7 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -586,6 +587,7 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1071,6 +1073,7 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -135,6 +135,7 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -650,6 +651,7 @@ async fn gateway_executes_kiro_claude_cli_sync_upstream_stream_via_local_finaliz
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -131,6 +131,7 @@ async fn gateway_executes_claude_chat_sync_same_format_via_local_finalize_respon
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -587,6 +588,7 @@ async fn gateway_executes_claude_chat_sync_upstream_stream_via_local_finalize_re
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1048,6 +1050,7 @@ async fn gateway_executes_claude_cli_sync_upstream_stream_via_local_finalize_res
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -136,6 +136,7 @@ async fn gateway_executes_gemini_chat_sync_same_format_via_local_finalize_respon
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -640,6 +641,7 @@ async fn gateway_executes_gemini_chat_sync_upstream_stream_via_local_finalize_re
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1134,6 +1136,7 @@ async fn gateway_executes_gemini_cli_sync_upstream_stream_via_local_finalize_res
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1642,6 +1645,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_upstream_stream_via_local_
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -111,6 +111,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -131,6 +131,7 @@ async fn gateway_executes_openai_chat_stream_via_local_decision_gate_without_exe
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -535,6 +536,7 @@ async fn gateway_executes_openai_chat_stream_via_local_openai_responses_cross_fo
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1020,6 +1022,7 @@ async fn gateway_executes_openai_chat_stream_via_local_cross_format_gemini_candi
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1458,6 +1461,7 @@ async fn gateway_executes_openai_chat_stream_with_custom_path_via_local_decision
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1961,6 +1965,7 @@ async fn gateway_retries_next_local_openai_chat_stream_candidate_after_retryable
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -131,6 +131,7 @@ async fn gateway_executes_codex_image_stream_via_local_decision_gate_after_oauth
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -517,6 +518,7 @@ async fn gateway_bridges_codex_image_sync_json_to_streaming_image_sse_impl() {
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -840,6 +842,7 @@ fn image_bridge_candidate_row(
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,
@@ -150,6 +150,7 @@ fn candidate_row() -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-ai-execute-stream-pii-redaction".to_string()]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -123,6 +123,7 @@ async fn gateway_executes_openai_responses_compact_as_unary_request_impl() {
priority: 1,
api_formats: Some(vec!["openai:responses:compact".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -136,6 +136,7 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -182,6 +182,7 @@ async fn gateway_executes_kiro_claude_cli_stream_via_local_provider_catalog_cand
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -700,6 +701,7 @@ async fn gateway_executes_claude_cli_stream_via_local_decision_gate_without_wait
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1161,6 +1163,7 @@ async fn gateway_executes_claude_code_cli_stream_via_local_decision_gate_with_lo
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1694,6 +1697,7 @@ async fn gateway_executes_claude_chat_stream_via_local_decision_gate_with_local_
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -101,6 +101,7 @@ async fn gateway_executes_gemini_chat_stream_via_local_decision_gate_with_local_
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -101,6 +101,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_with_local_s
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -553,6 +554,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1108,6 +1110,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_stream_via_local_decision_gate_wi
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1573,6 +1576,7 @@ async fn gateway_executes_antigravity_gemini_cli_stream_via_local_decision_gate_
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -91,6 +91,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -296,6 +297,7 @@ async fn gateway_skips_unsupported_local_openai_chat_sync_candidate_before_tryin
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -497,6 +499,7 @@ async fn gateway_surfaces_local_execution_runtime_miss_reason_when_all_openai_ch
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -775,6 +778,7 @@ async fn gateway_retries_next_local_openai_chat_sync_candidate_after_auth_failur
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -150,6 +150,7 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec!["endpoint-redaction-1".to_string()]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -454,6 +455,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -647,6 +649,7 @@ async fn gateway_executes_openai_chat_sync_via_local_decision_gate_without_execu
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]);
let candidate_selection_repository =
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
@@ -839,6 +842,7 @@ async fn gateway_executes_openai_chat_sync_with_regex_model_mapping_in_execution
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1130,6 +1134,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1151,6 +1156,7 @@ async fn gateway_executes_openai_chat_sync_via_local_cross_format_gemini_candida
priority: 2,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]);
row
}
@@ -1698,6 +1704,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_cli_syn
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2102,6 +2109,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2531,6 +2539,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_claude_sync_fa
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2939,6 +2948,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_sync_fa
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -3387,6 +3397,7 @@ async fn gateway_executes_openai_chat_sync_with_custom_path_via_local_decision_g
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -113,6 +113,7 @@ fn candidate_row(test_id: &str) -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: Some(vec![format!("endpoint-{test_id}")]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -129,6 +129,7 @@ async fn gateway_executes_claude_code_cli_sync_via_local_decision_gate_with_loca
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -197,6 +197,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -835,6 +836,7 @@ async fn gateway_executes_kiro_claude_cli_sync_via_local_provider_catalog_candid
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -124,6 +124,7 @@ async fn gateway_executes_claude_chat_sync_via_local_decision_gate_with_local_sy
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -694,6 +695,7 @@ async fn gateway_returns_claude_chat_error_for_local_sync_failure_impl() {
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -124,6 +124,7 @@ async fn gateway_executes_claude_cli_sync_via_local_decision_gate_with_local_syn
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -570,6 +571,7 @@ async fn gateway_returns_claude_cli_error_for_local_sync_failure_impl() {
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -853,6 +855,7 @@ async fn gateway_marks_claude_cli_cross_format_runtime_miss_when_format_conversi
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -141,6 +141,7 @@ async fn gateway_executes_openai_responses_sync_via_local_decision_gate_with_loc
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -650,6 +651,7 @@ async fn gateway_waits_for_api_key_concurrency_slot_then_executes_openai_respons
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1042,6 +1044,7 @@ async fn gateway_executes_openai_responses_sync_after_api_key_concurrency_wait_b
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1412,6 +1415,7 @@ async fn gateway_returns_openai_responses_error_for_local_sync_failure_impl() {
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1725,6 +1729,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_cl
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2131,6 +2136,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_sy
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2516,6 +2522,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_claude_ch
priority: 1,
api_formats: Some(vec!["claude:messages".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -2904,6 +2911,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_ch
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -3301,6 +3309,7 @@ async fn gateway_executes_codex_cli_sync_via_local_decision_gate_after_oauth_ref
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -123,6 +123,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_with_local_syn
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -561,6 +562,7 @@ async fn gateway_returns_gemini_cli_error_for_local_sync_failure_impl() {
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -864,6 +866,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1426,6 +1429,7 @@ async fn gateway_executes_vertex_ai_gemini_cli_sync_via_local_decision_gate_with
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1878,6 +1882,7 @@ async fn gateway_executes_antigravity_gemini_cli_sync_via_local_decision_gate_af
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -123,6 +123,7 @@ async fn gateway_executes_gemini_chat_sync_via_local_decision_gate_with_local_sy
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -551,6 +552,7 @@ async fn gateway_returns_gemini_chat_error_for_local_sync_failure_impl() {
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -129,6 +129,7 @@ async fn gateway_converts_openai_image_sync_to_gemini_image_provider_impl() {
priority: 1,
api_formats: Some(vec!["gemini:generate_content".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -481,6 +482,7 @@ async fn gateway_converts_gemini_image_sync_to_openai_image_provider_impl() {
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -833,6 +835,7 @@ async fn gateway_executes_codex_image_sync_via_local_decision_gate_after_oauth_r
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1242,6 +1245,7 @@ async fn gateway_plans_chatgpt_web_image_sync_with_internal_web_executor_url_imp
priority: 1,
api_formats: Some(vec!["openai:image".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -608,6 +608,7 @@ fn candidate_row(case: &RedactionFormatCase) -> StoredMinimalCandidateSelectionR
priority: 1,
api_formats: Some(vec![case.provider_format.api_format().to_string()]),
endpoint_ids: Some(vec![format!("endpoint-{}", case.test_id)]),
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -119,6 +119,7 @@ async fn gateway_executes_codex_search_with_responses_permission_and_search_cont
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,
+1
View File
@@ -112,6 +112,7 @@ fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -115,6 +115,7 @@ fn sample_files_candidate_row() -> StoredMinimalCandidateSelectionRow {
priority: 1,
api_formats: Some(vec!["gemini:files".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -291,6 +291,7 @@ fn sample_models_candidate_row(
priority: 1,
api_formats: Some(vec![api_format.to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -74,6 +74,7 @@ fn sample_codex_models_candidate_row(
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
},
]);
row
+1
View File
@@ -101,6 +101,7 @@ pub(super) fn sample_local_openai_candidate_row() -> StoredMinimalCandidateSelec
priority: 1,
api_formats: Some(vec!["openai:chat".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1611,6 +1611,7 @@ async fn gateway_records_failed_usage_when_all_local_claude_cli_candidates_are_s
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -1926,6 +1927,7 @@ fn gateway_keeps_failed_usage_request_capture_lightweight_for_large_local_claude
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -268,6 +268,7 @@ fn sample_candidate_row(spec: ProviderSpec) -> StoredMinimalCandidateSelectionRo
priority: 1,
api_formats: Some(vec![spec.api_format.to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(true),
model_is_active: true,
@@ -110,6 +110,7 @@ async fn gateway_executes_gemini_video_create_via_local_decision_gate_with_local
priority: 1,
api_formats: Some(vec!["gemini:video".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,
@@ -115,6 +115,7 @@ async fn gateway_executes_openai_video_create_via_local_decision_gate_with_local
priority: 1,
api_formats: Some(vec!["openai:video".to_string()]),
endpoint_ids: None,
operations: None,
}]),
model_supports_streaming: Some(false),
model_is_active: true,