centralize openai responses alias handling

This commit is contained in:
fawney19
2026-04-26 21:47:01 +08:00
parent ea3dc3257e
commit 4ec591fbf2
52 changed files with 364 additions and 396 deletions

View File

@@ -110,11 +110,7 @@ fn maybe_bridge_openai_image_sync_json_to_stream(
}
fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn is_standard_api_format(value: &str) -> bool {

View File

@@ -176,11 +176,7 @@ pub(crate) async fn rank_eligible_local_execution_candidates(
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -322,11 +322,7 @@ fn transport_key_supports_api_format(
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -36,10 +36,14 @@ pub(crate) fn resolve_same_format_provider_transport_unsupported_reason_for_trac
transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str,
) -> Option<&'static str> {
let provider_api_format = match provider_api_format.trim().to_ascii_lowercase().as_str() {
let provider_api_format = match crate::ai_pipeline::normalize_legacy_openai_format_alias(
provider_api_format,
)
.as_str()
{
"openai:chat" => "openai:chat",
"openai:responses" | "openai:cli" => "openai:responses",
"openai:responses:compact" | "openai:compact" => "openai:responses:compact",
"openai:responses" => "openai:responses",
"openai:responses:compact" => "openai:responses:compact",
"claude:chat" => "claude:chat",
"claude:cli" => "claude:cli",
"gemini:chat" => "gemini:chat",

View File

@@ -162,10 +162,11 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
let direct_auth = if kiro_auth.is_some() {
None
} else if same_format {
match provider_api_format {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format).as_str()
{
"gemini:cli" => resolve_local_gemini_auth(transport),
"claude:cli" => resolve_local_standard_auth(transport),
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact" => {
"openai:responses" | "openai:responses:compact" => {
resolve_local_openai_bearer_auth(transport)
}
_ => None,
@@ -535,15 +536,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts(
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::legacy_openai_format_alias_matches(left, right)
}
#[allow(clippy::too_many_arguments)]

View File

@@ -340,15 +340,7 @@ pub(crate) async fn materialize_local_openai_responses_candidate_attempts(
}
fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right)
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::legacy_openai_format_alias_matches(left, right)
}
pub(crate) async fn mark_skipped_local_openai_responses_candidate(
state: &AppState,

View File

@@ -71,11 +71,7 @@ fn diagnose_request_body_build_failure(
}
fn is_openai_responses_client_format(client_api_format: &str) -> bool {
let normalized = client_api_format.trim().to_ascii_lowercase();
matches!(
normalized.as_str(),
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact"
)
crate::ai_pipeline::is_openai_responses_family_format(client_api_format)
}
fn diagnose_same_format_provider_request_body_failure(

View File

@@ -26,8 +26,9 @@ pub(crate) use aether_ai_pipeline::api::{
find_kiro_real_thinking_end_tag_at_buffer_end, find_kiro_real_thinking_start_tag,
force_upstream_streaming_for_provider, generic_decision_missing_exact_provider_request,
implicit_sync_finalize_report_kind, is_core_error_finalize_kind, is_matching_stream_request,
kiro_crc32, map_claude_stop_reason, map_openai_reasoning_effort_to_claude_output,
map_openai_reasoning_effort_to_gemini_budget,
is_openai_responses_family_format, is_openai_responses_format, kiro_crc32,
legacy_openai_format_alias_matches, map_claude_stop_reason,
map_openai_reasoning_effort_to_claude_output, map_openai_reasoning_effort_to_gemini_budget,
maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_same_family_sync_body_from_normalized_payload,
@@ -36,7 +37,7 @@ pub(crate) use aether_ai_pipeline::api::{
maybe_build_standard_same_format_sync_body_from_normalized_payload,
maybe_build_standard_sync_finalize_product_from_normalized_payload,
normalize_claude_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request,
normalize_gemini_request_to_openai_chat_request, normalize_legacy_openai_format_alias,
normalize_openai_responses_request_to_openai_chat_request,
normalize_provider_private_report_context, normalize_provider_private_response_value,
normalize_standard_request_to_openai_chat_request, parse_direct_request_body,

View File

@@ -1,8 +1,8 @@
pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
match api_format {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
"openai:chat" => Some("openai:chat"),
"openai:responses" | "openai:cli" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"),
"openai:responses" => Some("openai:responses"),
"openai:responses:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"),
"openai:video" => Some("openai:video"),
_ => None,
@@ -10,10 +10,10 @@ pub(crate) fn normalized_signature(api_format: &str) -> Option<&'static str> {
}
pub(crate) fn local_path(api_format: &str) -> Option<&'static str> {
match api_format {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
"openai" | "openai:chat" => Some("/v1/chat/completions"),
"openai:responses" | "openai:cli" => Some("/v1/responses"),
"openai:responses:compact" | "openai:compact" => Some("/v1/responses/compact"),
"openai:responses" => Some("/v1/responses"),
"openai:responses:compact" => Some("/v1/responses/compact"),
"openai:image" => Some("/v1/images/generations"),
"openai:video" => Some("/v1/videos"),
_ => None,

View File

@@ -660,11 +660,7 @@ fn contains_api_format_or_alias(items: &[String], target: &str) -> bool {
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -21,11 +21,8 @@ pub(crate) fn should_bypass_execution_runtime_decision(
.as_deref()
.map(str::trim)
.unwrap_or_default()
.to_ascii_lowercase();
if !matches!(
provider_api_format.as_str(),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
.to_string();
if !crate::ai_pipeline::is_openai_responses_family_format(&provider_api_format) {
return false;
}
@@ -39,11 +36,7 @@ pub(crate) fn should_bypass_execution_runtime_decision(
}
pub(crate) fn should_bypass_execution_runtime_plan(plan: &ExecutionPlan) -> bool {
let provider_api_format = plan.provider_api_format.trim().to_ascii_lowercase();
if !matches!(
provider_api_format.as_str(),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
if !crate::ai_pipeline::is_openai_responses_family_format(&plan.provider_api_format) {
return false;
}

View File

@@ -90,10 +90,7 @@ pub(super) async fn maybe_handle(
let endpoint = match normalized_provider_type.as_str() {
"codex" => endpoints.into_iter().find(|endpoint| {
endpoint.is_active
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
&& crate::ai_pipeline::is_openai_responses_format(&endpoint.api_format)
}),
"antigravity" => endpoints.into_iter().find(|endpoint| {
endpoint.is_active

View File

@@ -227,10 +227,7 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
.await?;
if let Some(endpoint) = endpoints.into_iter().find(|endpoint| {
endpoint.is_active
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
&& crate::ai_pipeline::is_openai_responses_format(&endpoint.api_format)
}) {
let refreshed_key = state
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id))

View File

@@ -19,10 +19,7 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
.iter()
.find(|endpoint| {
endpoint.is_active
&& matches!(
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
&& crate::ai_pipeline::is_openai_responses_format(&endpoint.api_format)
})
.cloned(),
"antigravity" => endpoints

View File

@@ -289,11 +289,7 @@ fn provider_query_key_supports_endpoint(
}
fn provider_query_normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn provider_query_transport_supports_standard_test_execution(
@@ -301,11 +297,11 @@ fn provider_query_transport_supports_standard_test_execution(
transport: &AdminGatewayProviderTransportSnapshot,
api_format: &str,
) -> bool {
match api_format {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
"openai:chat" => {
crate::provider_transport::policy::supports_local_openai_chat_transport(transport)
}
"openai:responses" | "openai:cli" => {
"openai:responses" => {
crate::provider_transport::policy::supports_local_standard_transport_with_network(
transport, api_format,
)
@@ -933,7 +929,9 @@ async fn provider_query_execute_standard_test_candidate(
}
let provider_api_format = candidate.endpoint.api_format.as_str();
let provider_request_body = match provider_api_format {
let normalized_provider_api_format =
crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format);
let provider_request_body = match normalized_provider_api_format.as_str() {
"openai:chat" => {
let Some(mut provider_request_body) =
crate::ai_pipeline::build_local_openai_chat_request_body(
@@ -964,7 +962,7 @@ async fn provider_query_execute_standard_test_candidate(
crate::ai_pipeline::build_cross_format_openai_chat_request_body(
&request_body,
&candidate.effective_model,
provider_api_format,
normalized_provider_api_format.as_str(),
false,
)
else {
@@ -985,12 +983,12 @@ async fn provider_query_execute_standard_test_candidate(
}
provider_request_body
}
"openai:responses" | "openai:cli" => {
"openai:responses" => {
let Some(mut provider_request_body) =
crate::ai_pipeline::build_cross_format_openai_chat_request_body(
&request_body,
&candidate.effective_model,
provider_api_format,
normalized_provider_api_format.as_str(),
false,
)
else {
@@ -1037,13 +1035,19 @@ async fn provider_query_execute_standard_test_candidate(
} else {
None
};
let oauth_auth = match provider_api_format {
"openai:chat" | "openai:responses" | "openai:cli" | "claude:chat" | "claude:cli"
| "gemini:chat" | "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
let oauth_auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(
provider_api_format,
)
.as_str()
{
"openai:chat" | "openai:responses" | "claude:chat" | "claude:cli" | "gemini:chat"
| "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
_ => None,
};
let auth = match provider_api_format {
"openai:chat" | "openai:responses" | "openai:cli" => {
let auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format)
.as_str()
{
"openai:chat" | "openai:responses" => {
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
.or(oauth_auth)
}
@@ -1104,7 +1108,7 @@ async fn provider_query_execute_standard_test_candidate(
Some("application/json"),
)
}
"openai:responses" | "openai:cli" => {
"openai:responses" => {
crate::provider_transport::auth::build_complete_passthrough_headers_with_auth(
&parts.headers,
auth_header.as_deref().unwrap_or_default(),
@@ -1158,10 +1162,7 @@ async fn provider_query_execute_standard_test_candidate(
response_body: None,
});
}
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
) {
if crate::ai_pipeline::is_openai_responses_format(provider_api_format) {
crate::ai_pipeline::apply_codex_openai_responses_special_headers(
&mut request_headers,
&provider_request_body,
@@ -1266,8 +1267,8 @@ fn provider_query_prefers_chat_standard_test_api_format(api_format: &str) -> boo
fn provider_query_supports_cli_standard_test_api_format(api_format: &str) -> bool {
matches!(
api_format,
"openai:responses" | "openai:cli" | "claude:cli" | "gemini:cli"
crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:responses" | "claude:cli" | "gemini:cli"
)
}

View File

@@ -350,11 +350,7 @@ fn endpoint_matches_fixed_provider_template(
}
fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() {
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -313,14 +313,7 @@ impl<'a> AdminAppState<'a> {
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex"
&& matches!(
existing_endpoint
.api_format
.trim()
.to_ascii_lowercase()
.as_str(),
"openai:responses" | "openai:cli"
)
&& crate::ai_pipeline::is_openai_responses_format(&existing_endpoint.api_format)
{
let has_config_in_payload = fields.contains("config");
let config_payload = if has_config_in_payload {

View File

@@ -458,15 +458,13 @@ fn aggregate_sync_sse_response_for_client(
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match api_format {
match api_format.map(crate::ai_pipeline::normalize_legacy_openai_format_alias) {
Some(value) if value.eq_ignore_ascii_case("openai:chat") => {
aggregate_openai_chat_stream_sync_response(body)
}
Some(value)
if value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact") =>
|| value.eq_ignore_ascii_case("openai:responses:compact") =>
{
aggregate_openai_responses_stream_sync_response(body)
}
@@ -531,18 +529,10 @@ fn resolve_affinity_forward_client_api_format(
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
match api_format {
match api_format.map(crate::ai_pipeline::normalize_legacy_openai_format_alias) {
Some(value) if value.eq_ignore_ascii_case("openai:chat") => Some("openai:chat"),
Some(value)
if value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:cli") =>
{
Some("openai:responses")
}
Some(value)
if value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:compact") =>
{
Some(value) if value.eq_ignore_ascii_case("openai:responses") => Some("openai:responses"),
Some(value) if value.eq_ignore_ascii_case("openai:responses:compact") => {
Some("openai:responses:compact")
}
Some(value) if value.eq_ignore_ascii_case("claude:chat") => Some("claude:chat"),

View File

@@ -12,10 +12,10 @@ pub(crate) fn models_api_format(request_context: &GatewayPublicRequestContext) -
.and_then(|decision| decision.auth_endpoint_signature.as_deref())
.map(str::trim)
.filter(|signature| !signature.is_empty())?;
match signature.to_ascii_lowercase().as_str() {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(signature).as_str() {
"openai:chat" => Some("openai:chat"),
"openai:responses" | "openai:cli" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"),
"openai:responses" => Some("openai:responses"),
"openai:responses:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"),
"claude:chat" => Some("claude:chat"),
"gemini:chat" => Some("gemini:chat"),
@@ -37,12 +37,10 @@ const MODELS_CROSS_FORMAT_QUERY_API_FORMATS: &[&str] = &[
];
pub(super) fn models_query_api_formats(api_format: &str) -> &'static [&'static str] {
match api_format.trim().to_ascii_lowercase().as_str() {
match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "claude:chat"
| "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
"openai:image" => &["openai:image"],

View File

@@ -209,17 +209,17 @@ fn users_me_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAud
.or(item.endpoint_api_format.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty());
let Some(value) = api_format else {
return false;
};
matches!(
api_format,
Some(value)
if value.eq_ignore_ascii_case("openai:chat")
|| value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact")
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact")
|| value.eq_ignore_ascii_case("openai:image")
|| value.eq_ignore_ascii_case("claude:chat")
|| value.eq_ignore_ascii_case("claude:cli")
crate::ai_pipeline::normalize_legacy_openai_format_alias(value).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:image"
| "claude:chat"
| "claude:cli"
)
}

View File

@@ -2686,3 +2686,41 @@ fn ai_pipeline_openai_responses_specs_are_owned_by_pipeline_crate() {
);
}
}
#[test]
fn ai_pipeline_openai_cli_legacy_names_stay_out_of_primary_paths() {
for path in [
"crates/aether-ai-pipeline/src/contracts/plan_kinds.rs",
"crates/aether-ai-pipeline/src/planner/route.rs",
"crates/aether-ai-pipeline/src/planner/standard/openai_responses.rs",
"apps/aether-gateway/src/ai_pipeline/planner/decision/control_plan.rs",
"apps/aether-gateway/src/execution_runtime/fallback.rs",
] {
let source = read_workspace_file(path);
for forbidden in [
"openai:cli",
"openai:compact",
"openai_cli_",
"openai_compact_",
"OPENAI_CLI",
"OPENAI_COMPACT",
] {
assert!(
!source.contains(forbidden),
"{path} should not emit or branch on legacy OpenAI Responses aliases: {forbidden}"
);
}
}
let registry = read_workspace_file("crates/aether-ai-pipeline/src/conversion/registry.rs");
let implementation = registry
.split("#[cfg(test)]")
.next()
.expect("registry source should have an implementation section");
for forbidden in ["\"openai:cli\"", "\"openai:compact\""] {
assert!(
!implementation.contains(forbidden),
"conversion registry implementation should route legacy aliases through aether-ai-formats helpers: {forbidden}"
);
}
}