feat(openai): align GPT-5.6 and Codex request contracts

This commit is contained in:
MMEXA
2026-07-11 07:40:12 +08:00
parent bc1da3bf3f
commit dfa121dd5b
178 changed files with 17947 additions and 3541 deletions
+67 -27
View File
@@ -70,12 +70,10 @@ pub(crate) async fn request_model_local_rejection(
) {
if !contains_string(allowed_models, requested_model)
&& !model_directive_base_model_is_allowed_for_request(
state,
decision,
requested_model,
allowed_models,
)
.await
&& !request_model_resolves_to_allowed_model(
state,
decision,
@@ -417,18 +415,11 @@ fn estimate_text_tokens(text: &str) -> u64 {
chars.div_ceil(4).max(1)
}
async fn model_directive_base_model_is_allowed_for_request(
state: &AppState,
fn model_directive_base_model_is_allowed_for_request(
decision: &GatewayControlDecision,
requested_model: &str,
allowed_models: &[String],
) -> bool {
let Some(base_model) = crate::ai_serving::model_directive_base_model(requested_model) else {
return false;
};
if !contains_string(allowed_models, &base_model) {
return false;
}
let Some(client_api_format) = decision
.auth_endpoint_signature
.as_deref()
@@ -438,12 +429,12 @@ async fn model_directive_base_model_is_allowed_for_request(
return false;
};
for api_format in candidate_api_formats_for_model_resolution(&client_api_format) {
if crate::system_features::reasoning_model_directive_enabled_for_api_format_and_model(
state,
&api_format,
Some(requested_model),
)
.await
let resolution = decision
.model_directive_policy
.resolve_reasoning(&api_format, Some(requested_model));
if resolution
.base_model()
.is_some_and(|base_model| contains_string(allowed_models, base_model))
{
return true;
}
@@ -467,13 +458,10 @@ async fn request_model_resolves_to_allowed_model(
};
for api_format in candidate_api_formats_for_model_resolution(&client_api_format) {
let enable_model_directives =
crate::system_features::reasoning_model_directive_enabled_for_api_format_and_model(
state,
&api_format,
Some(requested_model),
)
.await;
let resolution = decision
.model_directive_policy
.resolve_reasoning(&api_format, Some(requested_model));
let routing_model = resolution.base_model().unwrap_or(requested_model);
let rows = state
.list_minimal_candidate_selection_rows_for_api_format(&api_format)
.await?;
@@ -482,18 +470,18 @@ async fn request_model_resolves_to_allowed_model(
.filter(|row| {
aether_scheduler_core::row_supports_requested_model_with_model_directives(
row,
requested_model,
routing_model,
&api_format,
enable_model_directives,
false,
)
})
.collect::<Vec<_>>();
let Some(resolved_global_model) =
aether_scheduler_core::resolve_requested_global_model_name_with_model_directives(
&matching_rows,
requested_model,
routing_model,
&api_format,
enable_model_directives,
false,
)
else {
continue;
@@ -877,6 +865,58 @@ mod tests {
);
}
#[tokio::test]
async fn model_rejection_reuses_request_policy_snapshot_for_directive_base_model() {
let state = state_with_rows(Vec::new());
let mut decision = decision_with_allowed_models(vec!["gpt-5.6-sol".to_string()]);
decision.model_directive_policy =
crate::system_features::ModelDirectivePolicySnapshot::from_config_values(
Some(&json!(true)),
None,
);
let uri: Uri = "/v1/chat/completions".parse().expect("uri should parse");
let body = Bytes::from_static(br#"{"model":"gpt-5.6-sol-high","messages":[]}"#);
let rejection =
request_model_local_rejection(&state, Some(&decision), &uri, &json_headers(), &body)
.await
.expect("model rejection should resolve");
assert_eq!(rejection, None);
}
#[tokio::test]
async fn model_rejection_uses_custom_policy_suffix_for_base_model_authorization() {
let state = state_with_rows(Vec::new());
let mut decision = decision_with_allowed_models(vec!["deployment-alias".to_string()]);
decision.model_directive_policy =
crate::system_features::ModelDirectivePolicySnapshot::from_config_values(
Some(&json!(true)),
Some(&json!({
"reasoning_effort": {
"api_formats": {
"openai:chat": {
"suffixes": ["VendorFuture"],
"mappings": {
"VendorFuture": { "reasoning_effort": "high" }
}
}
}
}
})),
);
let uri: Uri = "/v1/chat/completions".parse().expect("uri should parse");
let body =
Bytes::from_static(br#"{"model":"deployment-alias-VendorFuture","messages":[]}"#);
let rejection =
request_model_local_rejection(&state, Some(&decision), &uri, &json_headers(), &body)
.await
.expect("model rejection should resolve");
assert_eq!(rejection, None);
}
#[tokio::test]
async fn positive_balance_allows_unbounded_output_request_without_cost_estimate() {
let context = billing_context_with_pricing(
@@ -25,6 +25,7 @@ pub(crate) struct GatewayControlDecision {
pub(crate) auth_context: Option<GatewayControlAuthContext>,
pub(crate) admin_principal: Option<GatewayAdminPrincipalContext>,
pub(crate) local_auth_rejection: Option<GatewayLocalAuthRejection>,
pub(crate) model_directive_policy: crate::system_features::ModelDirectivePolicySnapshot,
}
impl GatewayControlDecision {
@@ -47,6 +48,7 @@ impl GatewayControlDecision {
auth_context: None,
admin_principal: None,
local_auth_rejection: None,
model_directive_policy: Default::default(),
}
}
@@ -131,6 +133,7 @@ impl ClassifiedRoute {
auth_context: None,
admin_principal: None,
local_auth_rejection: None,
model_directive_policy: Default::default(),
}
}
}
@@ -146,6 +149,10 @@ pub(crate) async fn resolve_control_route(
return Ok(None);
};
decision.public_query_string = uri.query().map(ToOwned::to_owned);
if decision.route_class.as_deref() == Some("ai_public") {
decision.model_directive_policy =
crate::system_features::ModelDirectivePolicySnapshot::load(state).await;
}
match resolve_control_decision_auth(state, headers, uri, trace_id, decision).await? {
ControlDecisionAuthResolution::Resolved(decision) => Ok(Some(decision)),
@@ -197,6 +204,15 @@ pub(super) fn detect_public_models_auth_signature(uri: &Uri, headers: &http::Hea
return "gemini:generate_content".to_string();
}
let has_codex_client_version = uri.path() == "/v1/models"
&& uri.query().is_some_and(|query| {
url::form_urlencoded::parse(query.as_bytes())
.any(|(key, value)| key == "client_version" && !value.trim().is_empty())
});
if has_codex_client_version {
return "openai:responses".to_string();
}
if uri.path().starts_with("/v1beta/models") {
return "gemini:generate_content".to_string();
}
@@ -20,6 +20,39 @@ fn classifies_models_list_as_public_support_route() {
assert!(!decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_codex_models_list_with_responses_auth_signature() {
let headers = headers(&[("authorization", "Bearer sk-test")]);
let uri: Uri = "/v1/models?client_version=0.144.1"
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::GET, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("public_support"));
assert_eq!(decision.route_family.as_deref(), Some("models"));
assert_eq!(decision.route_kind.as_deref(), Some("list"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("openai:responses")
);
}
#[test]
fn empty_codex_client_version_keeps_standard_openai_models_signature() {
let headers = headers(&[("authorization", "Bearer sk-test")]);
let uri: Uri = "/v1/models?client_version="
.parse()
.expect("uri should parse");
let decision =
classify_control_route(&http::Method::GET, &uri, &headers).expect("route should classify");
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("openai:chat")
);
}
#[test]
fn classifies_v1beta_models_as_gemini_public_support_route() {
let headers = headers(&[]);