mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
fix(gateway): route Responses compaction only to Responses providers
This commit is contained in:
@@ -86,6 +86,52 @@ impl GatewayLocalCandidatePreselectionPort<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A Responses compaction request carries the OpenAI-only `compaction_trigger`
|
||||
/// control item. It must stay on an OpenAI Responses endpoint: treating it as
|
||||
/// an ordinary cross-format request would make Gemini/Claude candidates look
|
||||
/// eligible and defer the inevitable lossy-conversion failure until payload
|
||||
/// construction.
|
||||
fn request_candidate_api_formats_for_operation(
|
||||
client_api_format: &str,
|
||||
require_streaming: bool,
|
||||
request_operation: Option<&str>,
|
||||
) -> Vec<String> {
|
||||
let candidate_api_formats =
|
||||
crate::ai_serving::request_candidate_api_formats(client_api_format, require_streaming)
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
restrict_candidate_api_formats_for_operation(
|
||||
client_api_format,
|
||||
request_operation,
|
||||
candidate_api_formats,
|
||||
)
|
||||
}
|
||||
|
||||
fn restrict_candidate_api_formats_for_operation(
|
||||
client_api_format: &str,
|
||||
request_operation: Option<&str>,
|
||||
candidate_api_formats: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let is_responses_compaction = request_operation.is_some_and(|operation| {
|
||||
operation.eq_ignore_ascii_case(crate::ai_serving::OPENAI_RESPONSES_OPERATION_COMPACT)
|
||||
});
|
||||
let is_standard_responses_client =
|
||||
crate::ai_serving::normalize_api_format_alias(client_api_format) == "openai:responses";
|
||||
if !(is_responses_compaction && is_standard_responses_client) {
|
||||
return candidate_api_formats;
|
||||
}
|
||||
|
||||
candidate_api_formats
|
||||
.into_iter()
|
||||
.filter(|candidate_api_format| {
|
||||
crate::ai_serving::normalize_api_format_alias(candidate_api_format)
|
||||
== "openai:responses"
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AiCandidatePreselectionPort for GatewayLocalCandidatePreselectionPort<'_> {
|
||||
type Candidate = SchedulerMinimalCandidateSelectionCandidate;
|
||||
@@ -219,11 +265,11 @@ pub(crate) async fn preselect_local_execution_candidates_with_serving(
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
let candidate_api_formats =
|
||||
crate::ai_serving::request_candidate_api_formats(client_api_format, require_streaming)
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let candidate_api_formats = request_candidate_api_formats_for_operation(
|
||||
client_api_format,
|
||||
require_streaming,
|
||||
request_operation,
|
||||
);
|
||||
preselect_local_execution_candidates_for_api_formats_with_serving(
|
||||
state,
|
||||
model_directive_policy,
|
||||
@@ -264,6 +310,11 @@ pub(crate) async fn preselect_local_execution_candidates_for_api_formats_with_se
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
let candidate_api_formats = restrict_candidate_api_formats_for_operation(
|
||||
client_api_format,
|
||||
request_operation,
|
||||
candidate_api_formats,
|
||||
);
|
||||
let model_directive_routing_models = resolve_model_directive_routing_models(
|
||||
model_directive_policy,
|
||||
&candidate_api_formats,
|
||||
@@ -362,11 +413,11 @@ impl<'a> LocalCandidatePreselectionPageCursor<'a> {
|
||||
allow_priority_page_cache: bool,
|
||||
trace_id: Option<&str>,
|
||||
) -> Self {
|
||||
let candidate_api_formats =
|
||||
crate::ai_serving::request_candidate_api_formats(client_api_format, require_streaming)
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
let candidate_api_formats = request_candidate_api_formats_for_operation(
|
||||
client_api_format,
|
||||
require_streaming,
|
||||
request_operation,
|
||||
);
|
||||
let model_directive_routing_models = resolve_model_directive_routing_models(
|
||||
model_directive_policy,
|
||||
&candidate_api_formats,
|
||||
@@ -1437,6 +1488,31 @@ mod tests {
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn compaction_operation_excludes_non_responses_provider_formats() {
|
||||
assert_eq!(
|
||||
request_candidate_api_formats_for_operation("openai:responses", true, Some("compact"),),
|
||||
vec!["openai:responses"]
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_formats_for_operation("openai:responses", true, None),
|
||||
vec![
|
||||
"openai:responses",
|
||||
"openai:chat",
|
||||
"claude:messages",
|
||||
"gemini:generate_content"
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
request_candidate_api_formats_for_operation(
|
||||
"openai:responses:compact",
|
||||
false,
|
||||
Some("compact"),
|
||||
),
|
||||
vec!["openai:responses:compact"]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct EmptyFallbackCountingRepository {
|
||||
fallback_reads: AtomicUsize,
|
||||
|
||||
@@ -180,7 +180,7 @@ pub(crate) use aether_ai_formats::{
|
||||
openai_responses_request_operation, openai_responses_synthetic_reasoning_item_id,
|
||||
strip_incompatible_openai_responses_reasoning_items,
|
||||
strip_incompatible_openai_responses_reasoning_items_with_policy, ApiOperation, ClientSurface,
|
||||
CODEX_CLIENT_VERSION,
|
||||
CODEX_CLIENT_VERSION, OPENAI_RESPONSES_OPERATION_COMPACT,
|
||||
};
|
||||
|
||||
pub(crate) fn plan_kind_matches_api_operation(
|
||||
|
||||
@@ -5433,6 +5433,26 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_compaction_trigger_is_not_converted_to_gemini() {
|
||||
let body = json!({
|
||||
"model": "gemini-3.7-flash",
|
||||
"input": [
|
||||
{"type": "message", "role": "user", "content": "hello"},
|
||||
{"type": "compaction_trigger"}
|
||||
]
|
||||
});
|
||||
|
||||
let error = convert_request_pure("openai:responses", "gemini:generate_content", &body)
|
||||
.expect_err("Gemini cannot represent the Responses compaction control item");
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
super::FormatError::LossyConversionBlocked { ref field, .. }
|
||||
if field == "input[1]"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_responses_rejects_official_multi_agent_incompatibilities() {
|
||||
let cases = [
|
||||
|
||||
Reference in New Issue
Block a user