fix(gateway): support current Codex Realtime live routes

This commit is contained in:
ZheFox
2026-08-23 02:51:49 +08:00
parent ea4453321d
commit 2cd20da1ec
18 changed files with 3540 additions and 254 deletions
@@ -214,6 +214,71 @@ mod tests {
assert_eq!(candidates[0].selected_provider_model_name, "gpt-5-canary-1");
}
#[test]
fn codex_live_enumeration_reuses_responses_mapping_without_widening_provider_scope() {
let mut codex = sample_row("codex-live");
codex.provider_name = "Codex".to_string();
codex.provider_type = "codex".to_string();
codex.endpoint_api_format = "codex:live".to_string();
codex.endpoint_api_family = Some("codex".to_string());
codex.endpoint_kind = Some("live".to_string());
codex.key_auth_type = "oauth".to_string();
codex.key_api_formats = Some(vec!["codex:live".to_string()]);
codex.key_allowed_models = Some(vec!["gpt-future-live".to_string()]);
codex.global_model_name = "live-future-alias".to_string();
codex.model_provider_model_name = "gpt-future-live".to_string();
codex.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-future-live".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: Some(vec![codex.endpoint_id.clone()]),
operations: None,
}]);
let constraints = SchedulerAuthConstraints {
allowed_providers: Some(vec!["codex".to_string()]),
allowed_api_formats: Some(vec!["codex:live".to_string()]),
allowed_models: Some(vec!["live-future-alias".to_string()]),
};
let candidates =
super::enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
rows: vec![codex.clone()],
normalized_api_format: "codex:live",
request_operation: None,
requested_model_name: "live-future-alias",
resolved_global_model_name: "live-future-alias",
require_streaming: true,
required_capabilities: None,
auth_constraints: Some(&constraints),
})
.expect("Codex Live candidate enumeration should build");
assert_eq!(candidates.len(), 1);
assert_eq!(
candidates[0].selected_provider_model_name,
"gpt-future-live"
);
for provider_type in ["openai", "custom"] {
let mut non_codex = codex.clone();
non_codex.provider_type = provider_type.to_string();
let candidates = super::enumerate_minimal_candidate_selection(
EnumerateMinimalCandidateSelectionInput {
rows: vec![non_codex],
normalized_api_format: "codex:live",
request_operation: None,
requested_model_name: "live-future-alias",
resolved_global_model_name: "live-future-alias",
require_streaming: true,
required_capabilities: None,
auth_constraints: None,
},
)
.expect("non-Codex Live candidate enumeration should build");
assert!(candidates.is_empty());
}
}
#[test]
fn enumeration_preserves_effective_streaming_capability() {
let mut row = sample_row("1");
+55 -8
View File
@@ -2,7 +2,8 @@ use std::borrow::Cow;
use std::collections::BTreeSet;
use aether_data_contracts::repository::candidate_selection::{
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
provider_model_mapping_api_format_covers, StoredMinimalCandidateSelectionRow,
StoredProviderModelMapping,
};
use aether_data_contracts::DataLayerError;
use regex::RegexBuilder;
@@ -339,9 +340,9 @@ fn mapping_scope_matches(
request_operation: Option<&str>,
) -> bool {
let api_format_matches_scope = mapping.api_formats.as_ref().is_none_or(|api_formats| {
api_formats
.iter()
.any(|value| api_format_scope_covers(value, api_format))
api_formats.iter().any(|value| {
provider_model_mapping_api_format_covers(&row.provider_type, value, api_format)
})
});
if !api_format_matches_scope {
return false;
@@ -498,10 +499,6 @@ fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format(left) == normalize_api_format(right)
}
fn api_format_scope_covers(allowed: &str, requested: &str) -> bool {
aether_ai_formats::api_format_permission_covers(allowed, requested)
}
fn requested_model_name_candidates(
requested_model_name: &str,
enable_model_directives: bool,
@@ -690,6 +687,56 @@ mod tests {
));
}
#[test]
fn codex_live_reuses_only_codex_responses_model_mappings() {
let mut row = sample_row("live-client-alias", "gpt-realtime-future");
row.provider_type = "codex".to_string();
row.endpoint_id = "endpoint-live".to_string();
row.endpoint_api_format = "codex:live".to_string();
row.key_auth_type = "oauth".to_string();
row.key_api_formats = Some(vec!["codex:live".to_string()]);
row.model_provider_model_mappings = Some(vec![StoredProviderModelMapping {
name: "gpt-realtime-future".to_string(),
priority: 1,
api_formats: Some(vec!["openai:responses".to_string()]),
endpoint_ids: Some(vec!["endpoint-live".to_string()]),
operations: None,
}]);
assert!(row_supports_requested_model(
&row,
"live-client-alias",
"codex:live"
));
assert_eq!(
resolve_provider_model_name(&row, "live-client-alias", "codex:live")
.map(|resolved| resolved.0),
Some("gpt-realtime-future".to_string())
);
let mut wrong_endpoint = row.clone();
wrong_endpoint.endpoint_id = "endpoint-other".to_string();
assert!(!row_supports_requested_model(
&wrong_endpoint,
"live-client-alias",
"codex:live"
));
for provider_type in ["openai", "custom"] {
let mut non_codex = row.clone();
non_codex.provider_type = provider_type.to_string();
assert!(!row_supports_requested_model(
&non_codex,
"live-client-alias",
"codex:live"
));
assert!(
resolve_provider_model_name(&non_codex, "live-client-alias", "codex:live")
.is_none()
);
}
}
#[test]
fn operation_scoped_mapping_overrides_generic_mapping_for_compaction() {
let mut row = sample_row("gpt-5.6-sol", "gpt-5.6-sol");