mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
feat(codex): 按操作语义路由 Responses V2 压缩
(cherry picked from commit 2fc604e047)
This commit is contained in:
@@ -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,
|
||||
|
||||
+7
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+18
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1188,6 +1188,7 @@ fn admin_usage_active_request_json(
|
||||
let mut value = json!({
|
||||
"id": item.id,
|
||||
"status": item.status,
|
||||
"request_type": item.request_type,
|
||||
"input_tokens": item.input_tokens,
|
||||
"effective_input_tokens": admin_usage_effective_input_tokens(item),
|
||||
"output_tokens": item.output_tokens,
|
||||
@@ -1304,6 +1305,7 @@ pub fn admin_usage_record_json(
|
||||
"status_code": item.status_code,
|
||||
"error_message": item.error_message,
|
||||
"status": item.status,
|
||||
"request_type": item.request_type,
|
||||
"has_fallback": admin_usage_has_fallback(item),
|
||||
"has_retry": false,
|
||||
"has_rectified": false,
|
||||
|
||||
@@ -8,6 +8,34 @@ pub mod stream;
|
||||
|
||||
const TOOL_ERROR_PREFIX: &str = "[tool error]";
|
||||
|
||||
/// Semantic operation carried by an OpenAI Responses request that asks the
|
||||
/// service to compact a thread. The request still uses the Responses wire
|
||||
/// contract and transport endpoint.
|
||||
pub const OPENAI_RESPONSES_OPERATION_COMPACT: &str = "compact";
|
||||
|
||||
/// Resolves the operation expressed by an OpenAI Responses wire request.
|
||||
///
|
||||
/// `responses_compaction_v2` is represented by a `compaction_trigger` input
|
||||
/// item on the normal Responses request. The legacy Compact API format is
|
||||
/// retained as the same operation for observability and scoped model mapping.
|
||||
pub fn openai_responses_request_operation(api_format: &str, body: &Value) -> Option<&'static str> {
|
||||
if aether_ai_formats::is_openai_responses_compact_format(api_format) {
|
||||
return Some(OPENAI_RESPONSES_OPERATION_COMPACT);
|
||||
}
|
||||
if !aether_ai_formats::is_openai_responses_format(api_format) {
|
||||
return None;
|
||||
}
|
||||
|
||||
body.get("input")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|items| {
|
||||
items
|
||||
.iter()
|
||||
.any(|item| item.get("type").and_then(Value::as_str) == Some("compaction_trigger"))
|
||||
})
|
||||
.then_some(OPENAI_RESPONSES_OPERATION_COMPACT)
|
||||
}
|
||||
|
||||
fn encode_tool_result_error(output: Value, is_error: bool) -> Value {
|
||||
if !is_error {
|
||||
return output;
|
||||
@@ -23,3 +51,41 @@ fn encode_tool_result_error(output: Value, is_error: bool) -> Value {
|
||||
Value::String(format!("{TOOL_ERROR_PREFIX}\n{detail}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{openai_responses_request_operation, OPENAI_RESPONSES_OPERATION_COMPACT};
|
||||
|
||||
#[test]
|
||||
fn resolves_compaction_trigger_as_compact_operation_on_responses_transport() {
|
||||
assert_eq!(
|
||||
openai_responses_request_operation(
|
||||
"openai:responses",
|
||||
&json!({
|
||||
"input": [
|
||||
{"role": "user", "content": "keep working"},
|
||||
{"type": "compaction_trigger"}
|
||||
]
|
||||
}),
|
||||
),
|
||||
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
|
||||
);
|
||||
assert_eq!(
|
||||
openai_responses_request_operation(
|
||||
"openai:responses",
|
||||
&json!({"input": [{"role": "user", "content": "keep working"}]}),
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_legacy_compact_contract_without_a_body_marker() {
|
||||
assert_eq!(
|
||||
openai_responses_request_operation("openai:responses:compact", &json!({})),
|
||||
Some(OPENAI_RESPONSES_OPERATION_COMPACT)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,9 @@ pub use formats::openai::responses::codex::{
|
||||
pub use formats::openai::responses::request::{
|
||||
validate_openai_responses_request_contract, OpenAiResponsesRequestContractViolation,
|
||||
};
|
||||
pub use formats::openai::responses::{
|
||||
openai_responses_request_operation, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
};
|
||||
pub use formats::registry::{
|
||||
build_stream_transcoder, convert_request, convert_request_pure,
|
||||
convert_request_pure_with_context, convert_response, convert_response_pure, emit_request_pure,
|
||||
|
||||
@@ -678,6 +678,7 @@ fn parse_embedded_provider_model_mappings(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]))
|
||||
}
|
||||
|
||||
@@ -698,6 +699,7 @@ fn parse_provider_model_mappings_array(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
@@ -742,6 +744,11 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
object.get("endpoint_ids").cloned(),
|
||||
"models.provider_model_mappings.endpoint_ids",
|
||||
)?;
|
||||
let operations = parse_string_list(
|
||||
object.get("operations").cloned(),
|
||||
"models.provider_model_mappings.operations",
|
||||
)?
|
||||
.and_then(normalize_request_operations);
|
||||
|
||||
Ok(Some(StoredProviderModelMapping {
|
||||
name: name.to_string(),
|
||||
@@ -752,9 +759,19 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
})?,
|
||||
api_formats,
|
||||
endpoint_ids,
|
||||
operations,
|
||||
}))
|
||||
}
|
||||
|
||||
fn 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 api_format_aliases(api_format: &str) -> Vec<String> {
|
||||
aether_ai_formats::api_format_storage_aliases(api_format)
|
||||
}
|
||||
|
||||
@@ -1352,6 +1352,7 @@ fn parse_embedded_provider_model_mappings(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]))
|
||||
}
|
||||
|
||||
@@ -1374,6 +1375,7 @@ fn parse_provider_model_mappings_array(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1430,6 +1432,11 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
object.get("endpoint_ids").cloned(),
|
||||
"models.provider_model_mappings.endpoint_ids",
|
||||
)?;
|
||||
let operations = parse_string_list(
|
||||
object.get("operations").cloned(),
|
||||
"models.provider_model_mappings.operations",
|
||||
)?
|
||||
.and_then(normalize_request_operations);
|
||||
|
||||
Ok(Some(StoredProviderModelMapping {
|
||||
name: name.to_string(),
|
||||
@@ -1440,9 +1447,19 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
})?,
|
||||
api_formats,
|
||||
endpoint_ids,
|
||||
operations,
|
||||
}))
|
||||
}
|
||||
|
||||
fn 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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -1626,7 +1643,7 @@ mod tests {
|
||||
#[test]
|
||||
fn parse_provider_model_mappings_accepts_stringified_array() {
|
||||
let parsed = parse_provider_model_mappings(Some(json!(
|
||||
"[{\"name\":\"gpt-5.2\",\"priority\":2,\"api_formats\":[\"openai:chat\"]}]"
|
||||
"[{\"name\":\"gpt-5.2\",\"priority\":2,\"api_formats\":[\"openai:chat\"],\"operations\":[\"COMPACT\"]}]"
|
||||
)))
|
||||
.expect("stringified provider_model_mappings should parse");
|
||||
|
||||
@@ -1637,6 +1654,7 @@ mod tests {
|
||||
priority: 2,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: Some(vec!["compact".to_string()]),
|
||||
}])
|
||||
);
|
||||
}
|
||||
@@ -1653,6 +1671,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}])
|
||||
);
|
||||
}
|
||||
@@ -1676,12 +1695,14 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
},
|
||||
StoredProviderModelMapping {
|
||||
name: "gpt-5.2-mini".to_string(),
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
@@ -1071,6 +1071,7 @@ fn parse_embedded_provider_model_mappings(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]))
|
||||
}
|
||||
|
||||
@@ -1091,6 +1092,7 @@ fn parse_provider_model_mappings_array(
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
@@ -1135,6 +1137,11 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
object.get("endpoint_ids").cloned(),
|
||||
"models.provider_model_mappings.endpoint_ids",
|
||||
)?;
|
||||
let operations = parse_string_list(
|
||||
object.get("operations").cloned(),
|
||||
"models.provider_model_mappings.operations",
|
||||
)?
|
||||
.and_then(normalize_request_operations);
|
||||
|
||||
Ok(Some(StoredProviderModelMapping {
|
||||
name: name.to_string(),
|
||||
@@ -1145,9 +1152,19 @@ fn parse_provider_model_mapping_object_lenient(
|
||||
})?,
|
||||
api_formats,
|
||||
endpoint_ids,
|
||||
operations,
|
||||
}))
|
||||
}
|
||||
|
||||
fn 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 api_format_aliases(api_format: &str) -> Vec<String> {
|
||||
aether_ai_formats::api_format_storage_aliases(api_format)
|
||||
}
|
||||
@@ -1212,6 +1229,14 @@ mod tests {
|
||||
Some(vec!["alias-global".to_string()])
|
||||
);
|
||||
assert_eq!(rows[1].global_model_supports_streaming, Some(true));
|
||||
assert_eq!(
|
||||
rows[1]
|
||||
.model_provider_model_mappings
|
||||
.as_ref()
|
||||
.and_then(|mappings| mappings.first())
|
||||
.and_then(|mapping| mapping.operations.as_ref()),
|
||||
Some(&vec!["compact".to_string()])
|
||||
);
|
||||
|
||||
let requested = repository
|
||||
.list_for_exact_api_format_and_requested_model_page(
|
||||
@@ -1393,7 +1418,7 @@ INSERT INTO models (
|
||||
)
|
||||
VALUES (
|
||||
'model-1', 'provider-1', 'global-1', 'provider-model',
|
||||
'[{"name":"alias-provider","api_formats":["openai:chat"],"priority":1}]',
|
||||
'[{"name":"alias-provider","api_formats":["openai:chat"],"operations":["COMPACT"],"priority":1}]',
|
||||
1, 1, 1, 1, 1
|
||||
),
|
||||
(
|
||||
|
||||
@@ -7,6 +7,10 @@ pub struct StoredProviderModelMapping {
|
||||
pub api_formats: Option<Vec<String>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint_ids: Option<Vec<String>>,
|
||||
/// Optional request-operation scope. An omitted scope applies to every
|
||||
/// operation supported by the selected API format.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub operations: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
|
||||
@@ -426,6 +426,7 @@ mod tests {
|
||||
priority: 0,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
let repository = InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
mapped,
|
||||
@@ -458,6 +459,7 @@ mod tests {
|
||||
priority: 0,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
|
||||
let mut responses = search.clone();
|
||||
@@ -521,6 +523,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: Some(vec!["endpoint-openai".to_string()]),
|
||||
operations: None,
|
||||
}]);
|
||||
|
||||
let mut scoped_out = selected.clone();
|
||||
|
||||
@@ -27,6 +27,7 @@ fn enumerate_minimal_candidate_selection_inner(
|
||||
let EnumerateMinimalCandidateSelectionInput {
|
||||
rows,
|
||||
normalized_api_format,
|
||||
request_operation,
|
||||
requested_model_name,
|
||||
resolved_global_model_name,
|
||||
require_streaming,
|
||||
@@ -63,11 +64,12 @@ fn enumerate_minimal_candidate_selection_inner(
|
||||
continue;
|
||||
}
|
||||
let Some((selected_provider_model_name, mapping_matched_model)) =
|
||||
crate::resolve_provider_model_name_with_model_directives(
|
||||
crate::resolve_provider_model_name_with_model_directives_and_request_operation(
|
||||
&row,
|
||||
requested_model_name,
|
||||
normalized_api_format,
|
||||
enable_model_directives,
|
||||
request_operation,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
|
||||
@@ -71,6 +71,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]),
|
||||
model_supports_streaming: None,
|
||||
model_is_active: true,
|
||||
@@ -199,6 +200,7 @@ mod tests {
|
||||
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
|
||||
rows: vec![sample_row("1"), disallowed],
|
||||
normalized_api_format: "openai:chat",
|
||||
request_operation: None,
|
||||
requested_model_name: "gpt-5",
|
||||
resolved_global_model_name: "gpt-5",
|
||||
require_streaming: false,
|
||||
@@ -221,6 +223,7 @@ mod tests {
|
||||
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
|
||||
rows: vec![row],
|
||||
normalized_api_format: "openai:chat",
|
||||
request_operation: None,
|
||||
requested_model_name: "gpt-5",
|
||||
resolved_global_model_name: "gpt-5",
|
||||
require_streaming: false,
|
||||
@@ -244,6 +247,7 @@ mod tests {
|
||||
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
|
||||
rows: vec![later_priority, earlier_priority],
|
||||
normalized_api_format: "openai:chat",
|
||||
request_operation: None,
|
||||
requested_model_name: "gpt-5",
|
||||
resolved_global_model_name: "gpt-5",
|
||||
require_streaming: false,
|
||||
@@ -295,6 +299,7 @@ mod tests {
|
||||
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
|
||||
rows: vec![missing_capability, matching_capability],
|
||||
normalized_api_format: "openai:chat",
|
||||
request_operation: None,
|
||||
requested_model_name: "gpt-5",
|
||||
resolved_global_model_name: "gpt-5",
|
||||
require_streaming: false,
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct SchedulerMinimalCandidateSelectionCandidate {
|
||||
pub struct EnumerateMinimalCandidateSelectionInput<'a> {
|
||||
pub rows: Vec<StoredMinimalCandidateSelectionRow>,
|
||||
pub normalized_api_format: &'a str,
|
||||
pub request_operation: Option<&'a str>,
|
||||
pub requested_model_name: &'a str,
|
||||
pub resolved_global_model_name: &'a str,
|
||||
pub require_streaming: bool,
|
||||
|
||||
@@ -40,10 +40,13 @@ pub use health::{
|
||||
pub use model::{
|
||||
candidate_model_names, extract_global_priority_for_format, matches_model_mapping,
|
||||
normalize_api_format, resolve_provider_model_name,
|
||||
resolve_provider_model_name_with_model_directives, resolve_requested_global_model_name,
|
||||
resolve_requested_global_model_name_with_model_directives, row_supports_requested_model,
|
||||
row_supports_requested_model_with_model_directives, row_supports_required_capability,
|
||||
select_provider_model_name,
|
||||
resolve_provider_model_name_with_model_directives,
|
||||
resolve_provider_model_name_with_model_directives_and_request_operation,
|
||||
resolve_requested_global_model_name, resolve_requested_global_model_name_with_model_directives,
|
||||
resolve_requested_global_model_name_with_model_directives_and_request_operation,
|
||||
row_supports_requested_model, row_supports_requested_model_with_model_directives,
|
||||
row_supports_requested_model_with_model_directives_and_request_operation,
|
||||
row_supports_required_capability, select_provider_model_name,
|
||||
};
|
||||
pub use provider::{build_provider_concurrent_limit_map, should_skip_provider_quota};
|
||||
pub use ranking::{
|
||||
|
||||
@@ -25,17 +25,33 @@ pub fn resolve_requested_global_model_name_with_model_directives(
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
) -> Option<String> {
|
||||
resolve_requested_global_model_name_with_model_directives_and_request_operation(
|
||||
rows,
|
||||
requested_model_name,
|
||||
api_format,
|
||||
enable_model_directives,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_requested_global_model_name_with_model_directives_and_request_operation(
|
||||
rows: &[StoredMinimalCandidateSelectionRow],
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
request_operation: Option<&str>,
|
||||
) -> Option<String> {
|
||||
requested_model_name_candidates(requested_model_name, enable_model_directives).find_map(
|
||||
|requested_model_name| {
|
||||
let requested_model_name = requested_model_name.as_ref();
|
||||
resolve_global_model_name_by(rows, |row| {
|
||||
row_has_available_provider_model(row, api_format)
|
||||
row_has_available_provider_model(row, api_format, request_operation)
|
||||
&& row.global_model_name == requested_model_name
|
||||
})
|
||||
.or_else(|| {
|
||||
resolve_global_model_name_by(rows, |row| {
|
||||
row_default_provider_model_name_available(row, api_format)
|
||||
row_default_provider_model_name_available(row, api_format, request_operation)
|
||||
&& row.model_provider_model_name == requested_model_name
|
||||
})
|
||||
})
|
||||
@@ -45,7 +61,7 @@ pub fn resolve_requested_global_model_name_with_model_directives(
|
||||
.as_ref()
|
||||
.is_some_and(|mappings| {
|
||||
mappings.iter().any(|mapping| {
|
||||
mapping_scope_matches(mapping, row, api_format)
|
||||
mapping_scope_matches(mapping, row, api_format, request_operation)
|
||||
&& mapping.name == requested_model_name
|
||||
})
|
||||
})
|
||||
@@ -53,7 +69,7 @@ pub fn resolve_requested_global_model_name_with_model_directives(
|
||||
})
|
||||
.or_else(|| {
|
||||
resolve_global_model_name_by(rows, |row| {
|
||||
row_has_available_provider_model(row, api_format)
|
||||
row_has_available_provider_model(row, api_format, request_operation)
|
||||
&& row.global_model_mappings.as_ref().is_some_and(|patterns| {
|
||||
patterns
|
||||
.iter()
|
||||
@@ -78,10 +94,31 @@ pub fn row_supports_requested_model_with_model_directives(
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
) -> bool {
|
||||
row_supports_requested_model_with_model_directives_and_request_operation(
|
||||
row,
|
||||
requested_model_name,
|
||||
api_format,
|
||||
enable_model_directives,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn row_supports_requested_model_with_model_directives_and_request_operation(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
request_operation: Option<&str>,
|
||||
) -> bool {
|
||||
requested_model_name_candidates(requested_model_name, enable_model_directives).any(
|
||||
|requested_model_name| {
|
||||
row_supports_requested_model_exact(row, requested_model_name.as_ref(), api_format)
|
||||
row_supports_requested_model_exact(
|
||||
row,
|
||||
requested_model_name.as_ref(),
|
||||
api_format,
|
||||
request_operation,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -90,10 +127,11 @@ fn row_supports_requested_model_exact(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
) -> bool {
|
||||
row_has_available_provider_model(row, api_format)
|
||||
row_has_available_provider_model(row, api_format, request_operation)
|
||||
&& (row.global_model_name == requested_model_name
|
||||
|| (row_default_provider_model_name_available(row, api_format)
|
||||
|| (row_default_provider_model_name_available(row, api_format, request_operation)
|
||||
&& row.model_provider_model_name == requested_model_name)
|
||||
|| row.global_model_mappings.as_ref().is_some_and(|patterns| {
|
||||
patterns
|
||||
@@ -105,7 +143,7 @@ fn row_supports_requested_model_exact(
|
||||
.as_ref()
|
||||
.is_some_and(|mappings| {
|
||||
mappings.iter().any(|mapping| {
|
||||
mapping_scope_matches(mapping, row, api_format)
|
||||
mapping_scope_matches(mapping, row, api_format, request_operation)
|
||||
&& mapping.name == requested_model_name
|
||||
})
|
||||
})
|
||||
@@ -145,7 +183,24 @@ pub fn resolve_provider_model_name_with_model_directives(
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
) -> Option<(String, Option<String>)> {
|
||||
let selected_provider_model_name = resolve_selected_provider_model_name(row, api_format)?;
|
||||
resolve_provider_model_name_with_model_directives_and_request_operation(
|
||||
row,
|
||||
requested_model_name,
|
||||
api_format,
|
||||
enable_model_directives,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_provider_model_name_with_model_directives_and_request_operation(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
requested_model_name: &str,
|
||||
api_format: &str,
|
||||
enable_model_directives: bool,
|
||||
request_operation: Option<&str>,
|
||||
) -> Option<(String, Option<String>)> {
|
||||
let selected_provider_model_name =
|
||||
resolve_selected_provider_model_name(row, api_format, request_operation)?;
|
||||
let Some(key_allowed_models) = row.key_allowed_models.as_ref() else {
|
||||
return Some((selected_provider_model_name, None));
|
||||
};
|
||||
@@ -175,7 +230,7 @@ pub fn resolve_provider_model_name_with_model_directives(
|
||||
sorted_allowed_models.sort_unstable();
|
||||
|
||||
for &allowed_model in &sorted_allowed_models {
|
||||
if row_has_candidate_model_name(row, api_format, allowed_model) {
|
||||
if row_has_candidate_model_name(row, api_format, request_operation, allowed_model) {
|
||||
let allowed_model = allowed_model.to_owned();
|
||||
return Some((selected_provider_model_name.clone(), Some(allowed_model)));
|
||||
}
|
||||
@@ -198,13 +253,14 @@ pub fn select_provider_model_name(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
) -> String {
|
||||
resolve_selected_provider_model_name(row, api_format)
|
||||
resolve_selected_provider_model_name(row, api_format, None)
|
||||
.unwrap_or_else(|| row.model_provider_model_name.clone())
|
||||
}
|
||||
|
||||
fn resolve_selected_provider_model_name(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let Some(mappings) = row.model_provider_model_mappings.as_ref() else {
|
||||
return Some(row.model_provider_model_name.clone());
|
||||
@@ -212,17 +268,20 @@ fn resolve_selected_provider_model_name(
|
||||
|
||||
if let Some(mapping) = mappings
|
||||
.iter()
|
||||
.filter(|mapping| mapping_scope_matches(mapping, row, api_format))
|
||||
.filter(|mapping| mapping_scope_matches(mapping, row, api_format, request_operation))
|
||||
.min_by(|left, right| {
|
||||
left.priority
|
||||
.cmp(&right.priority)
|
||||
.then_with(|| {
|
||||
mapping_operation_scope_rank(right).cmp(&mapping_operation_scope_rank(left))
|
||||
})
|
||||
.then(left.name.cmp(&right.name))
|
||||
})
|
||||
{
|
||||
return Some(mapping.name.clone());
|
||||
}
|
||||
|
||||
row_default_provider_model_name_available(row, api_format)
|
||||
row_default_provider_model_name_available(row, api_format, request_operation)
|
||||
.then(|| row.model_provider_model_name.clone())
|
||||
}
|
||||
|
||||
@@ -231,12 +290,12 @@ pub fn candidate_model_names(
|
||||
api_format: &str,
|
||||
) -> BTreeSet<String> {
|
||||
let mut names = BTreeSet::new();
|
||||
if row_default_provider_model_name_available(row, api_format) {
|
||||
if row_default_provider_model_name_available(row, api_format, None) {
|
||||
names.insert(row.model_provider_model_name.clone());
|
||||
}
|
||||
if let Some(mappings) = row.model_provider_model_mappings.as_ref() {
|
||||
for mapping in mappings {
|
||||
if mapping_scope_matches(mapping, row, api_format) {
|
||||
if mapping_scope_matches(mapping, row, api_format, None) {
|
||||
names.insert(mapping.name.clone());
|
||||
}
|
||||
}
|
||||
@@ -247,13 +306,15 @@ pub fn candidate_model_names(
|
||||
fn row_has_available_provider_model(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
) -> bool {
|
||||
resolve_selected_provider_model_name(row, api_format).is_some()
|
||||
resolve_selected_provider_model_name(row, api_format, request_operation).is_some()
|
||||
}
|
||||
|
||||
fn row_default_provider_model_name_available(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
) -> bool {
|
||||
let Some(mappings) = row.model_provider_model_mappings.as_ref() else {
|
||||
return true;
|
||||
@@ -264,7 +325,7 @@ fn row_default_provider_model_name_available(
|
||||
continue;
|
||||
}
|
||||
has_explicit_default_mapping = true;
|
||||
if mapping_scope_matches(mapping, row, api_format) {
|
||||
if mapping_scope_matches(mapping, row, api_format, request_operation) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -275,6 +336,7 @@ fn mapping_scope_matches(
|
||||
mapping: &StoredProviderModelMapping,
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
) -> bool {
|
||||
let api_format_matches_scope = mapping.api_formats.as_ref().is_none_or(|api_formats| {
|
||||
api_formats
|
||||
@@ -285,13 +347,28 @@ fn mapping_scope_matches(
|
||||
return false;
|
||||
}
|
||||
|
||||
mapping.endpoint_ids.as_ref().is_none_or(|endpoint_ids| {
|
||||
let endpoint_matches_scope = mapping.endpoint_ids.as_ref().is_none_or(|endpoint_ids| {
|
||||
endpoint_ids
|
||||
.iter()
|
||||
.any(|endpoint_id| endpoint_id == &row.endpoint_id)
|
||||
});
|
||||
if !endpoint_matches_scope {
|
||||
return false;
|
||||
}
|
||||
|
||||
mapping.operations.as_ref().is_none_or(|operations| {
|
||||
request_operation.is_some_and(|request_operation| {
|
||||
operations
|
||||
.iter()
|
||||
.any(|operation| operation.eq_ignore_ascii_case(request_operation))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn mapping_operation_scope_rank(mapping: &StoredProviderModelMapping) -> u8 {
|
||||
u8::from(mapping.operations.is_some())
|
||||
}
|
||||
|
||||
pub fn row_supports_required_capability(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
required_capability: &str,
|
||||
@@ -401,16 +478,18 @@ pub fn normalize_api_format(value: &str) -> String {
|
||||
fn row_has_candidate_model_name(
|
||||
row: &StoredMinimalCandidateSelectionRow,
|
||||
api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
model_name: &str,
|
||||
) -> bool {
|
||||
(row_default_provider_model_name_available(row, api_format)
|
||||
(row_default_provider_model_name_available(row, api_format, request_operation)
|
||||
&& row.model_provider_model_name == model_name)
|
||||
|| row
|
||||
.model_provider_model_mappings
|
||||
.as_ref()
|
||||
.is_some_and(|mappings| {
|
||||
mappings.iter().any(|mapping| {
|
||||
mapping_scope_matches(mapping, row, api_format) && mapping.name == model_name
|
||||
mapping_scope_matches(mapping, row, api_format, request_operation)
|
||||
&& mapping.name == model_name
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -484,6 +563,7 @@ mod tests {
|
||||
use super::{
|
||||
matches_model_mapping, resolve_provider_model_name,
|
||||
resolve_provider_model_name_with_model_directives,
|
||||
resolve_provider_model_name_with_model_directives_and_request_operation,
|
||||
resolve_requested_global_model_name_with_model_directives, row_supports_requested_model,
|
||||
row_supports_requested_model_with_model_directives,
|
||||
};
|
||||
@@ -518,6 +598,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:chat".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
|
||||
let resolved = resolve_provider_model_name(&row, "gpt-5", "openai:chat")
|
||||
@@ -581,6 +662,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:responses".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
|
||||
assert!(row_supports_requested_model(
|
||||
@@ -599,6 +681,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: Some(vec!["openai:search".to_string()]),
|
||||
endpoint_ids: None,
|
||||
operations: None,
|
||||
}]);
|
||||
assert!(!row_supports_requested_model(
|
||||
&row,
|
||||
@@ -607,6 +690,51 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_scoped_mapping_overrides_generic_mapping_for_compaction() {
|
||||
let mut row = sample_row("gpt-5.6-sol", "gpt-5.6-sol");
|
||||
row.endpoint_api_format = "openai:responses".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()]),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
resolve_provider_model_name_with_model_directives_and_request_operation(
|
||||
&row,
|
||||
"gpt-5.6-sol",
|
||||
"openai:responses",
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.map(|resolved| resolved.0),
|
||||
Some("gpt-5.6-sol".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_provider_model_name_with_model_directives_and_request_operation(
|
||||
&row,
|
||||
"gpt-5.6-sol",
|
||||
"openai:responses",
|
||||
false,
|
||||
Some("compact"),
|
||||
)
|
||||
.map(|resolved| resolved.0),
|
||||
Some("gpt-5.6-terra".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_directive_suffix_prefers_exact_model_before_base_fallback() {
|
||||
let exact = sample_row("gpt-5.4-high", "gpt-5.4-high-upstream");
|
||||
@@ -685,6 +813,7 @@ mod tests {
|
||||
priority: 1,
|
||||
api_formats: None,
|
||||
endpoint_ids: Some(vec!["endpoint-openai".to_string()]),
|
||||
operations: None,
|
||||
}]);
|
||||
|
||||
assert!(!row_supports_requested_model(
|
||||
|
||||
@@ -254,8 +254,13 @@ pub fn build_lifecycle_usage_seed(
|
||||
let model = context_string(context, "model")
|
||||
.or_else(|| non_empty_str(plan.model_name.as_deref()))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let request_type =
|
||||
infer_request_type_from_contracts(api_format.as_deref(), endpoint_api_format.as_deref());
|
||||
let provider_request = context_body_value(context, "provider_request_body")
|
||||
.or_else(|| plan_json_body_capture_for_usage(plan));
|
||||
let request_type = infer_request_type_from_contracts(
|
||||
api_format.as_deref(),
|
||||
endpoint_api_format.as_deref(),
|
||||
provider_request.as_ref(),
|
||||
);
|
||||
let api_family = api_format
|
||||
.as_deref()
|
||||
.and_then(infer_api_family)
|
||||
@@ -804,6 +809,7 @@ pub fn build_terminal_usage_context_seed(
|
||||
let request_type = infer_request_type_from_contracts(
|
||||
Some(client_contract.as_str()),
|
||||
Some(provider_contract.as_str()),
|
||||
request_capture.provider_request.as_ref(),
|
||||
);
|
||||
let has_format_conversion = resolve_has_format_conversion(
|
||||
context,
|
||||
@@ -1697,6 +1703,7 @@ fn build_usage_event_data_seed_with_detail(
|
||||
let request_type = Some(infer_request_type_from_contracts(
|
||||
api_format.as_deref(),
|
||||
endpoint_api_format.as_deref(),
|
||||
request_capture.provider_request.as_ref(),
|
||||
));
|
||||
let api_family = api_format
|
||||
.as_deref()
|
||||
@@ -2467,7 +2474,20 @@ fn infer_request_type(api_format: Option<&str>) -> String {
|
||||
fn infer_request_type_from_contracts(
|
||||
client_api_format: Option<&str>,
|
||||
provider_api_format: Option<&str>,
|
||||
provider_request: Option<&Value>,
|
||||
) -> String {
|
||||
let empty_body = Value::Null;
|
||||
let provider_request = provider_request.unwrap_or(&empty_body);
|
||||
for api_format in [provider_api_format, client_api_format]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
if let Some(operation) =
|
||||
aether_ai_formats::openai_responses_request_operation(api_format, provider_request)
|
||||
{
|
||||
return operation.to_string();
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
infer_endpoint_kind(provider_api_format.unwrap_or_default()),
|
||||
Some("image")
|
||||
@@ -3711,7 +3731,9 @@ mod tests {
|
||||
"candidate_id": "cand-pending-event-1",
|
||||
"candidate_index": 3,
|
||||
"original_request_body": {"messages": [{"content": "omit me"}]},
|
||||
"provider_request_body": {"input": "omit me too"}
|
||||
"provider_request_body": {
|
||||
"input": [{"type": "compaction_trigger"}]
|
||||
}
|
||||
})),
|
||||
),
|
||||
1_700_000_020,
|
||||
@@ -3723,6 +3745,7 @@ mod tests {
|
||||
assert_eq!(record.request_id, "req-pending-event-1");
|
||||
assert_eq!(record.status, "pending");
|
||||
assert_eq!(record.billing_status, "pending");
|
||||
assert_eq!(record.request_type.as_deref(), Some("compact"));
|
||||
assert_eq!(record.finalized_at_unix_secs, None);
|
||||
assert_eq!(record.updated_at_unix_secs, 1_700_000_020);
|
||||
assert!(record.request_body.is_none());
|
||||
|
||||
Reference in New Issue
Block a user