mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
feat(openai): align GPT-5.6 and Codex request contracts
This commit is contained in:
@@ -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(&[]);
|
||||
|
||||
Reference in New Issue
Block a user