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

6
Cargo.lock generated
View File

@@ -23,6 +23,7 @@ dependencies = [
name = "aether-admin" name = "aether-admin"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"aether-billing", "aether-billing",
"aether-contracts", "aether-contracts",
"aether-data", "aether-data",
@@ -113,6 +114,7 @@ dependencies = [
name = "aether-data" name = "aether-data"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"aether-cache", "aether-cache",
"aether-data-contracts", "aether-data-contracts",
"aether-wallet", "aether-wallet",
@@ -136,6 +138,7 @@ dependencies = [
name = "aether-data-contracts" name = "aether-data-contracts"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"async-trait", "async-trait",
"serde", "serde",
"serde_json", "serde_json",
@@ -213,6 +216,7 @@ dependencies = [
name = "aether-model-fetch" name = "aether-model-fetch"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"aether-contracts", "aether-contracts",
"aether-data-contracts", "aether-data-contracts",
"aether-provider-transport", "aether-provider-transport",
@@ -317,6 +321,7 @@ dependencies = [
name = "aether-scheduler-core" name = "aether-scheduler-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"aether-contracts", "aether-contracts",
"aether-data-contracts", "aether-data-contracts",
"aether-wallet", "aether-wallet",
@@ -352,6 +357,7 @@ dependencies = [
name = "aether-usage-runtime" name = "aether-usage-runtime"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aether-ai-formats",
"aether-contracts", "aether-contracts",
"aether-data", "aether-data",
"aether-data-contracts", "aether-data-contracts",

View File

@@ -110,11 +110,7 @@ fn maybe_bridge_openai_image_sync_json_to_stream(
} }
fn normalize_api_format(value: &str) -> String { fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn is_standard_api_format(value: &str) -> bool { 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 { fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { 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 { fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { 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, transport: &GatewayProviderTransportSnapshot,
provider_api_format: &str, provider_api_format: &str,
) -> Option<&'static 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:chat" => "openai:chat",
"openai:responses" | "openai:cli" => "openai:responses", "openai:responses" => "openai:responses",
"openai:responses:compact" | "openai:compact" => "openai:responses:compact", "openai:responses:compact" => "openai:responses:compact",
"claude:chat" => "claude:chat", "claude:chat" => "claude:chat",
"claude:cli" => "claude:cli", "claude:cli" => "claude:cli",
"gemini:chat" => "gemini:chat", "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() { let direct_auth = if kiro_auth.is_some() {
None None
} else if same_format { } 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), "gemini:cli" => resolve_local_gemini_auth(transport),
"claude:cli" => resolve_local_standard_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) resolve_local_openai_bearer_auth(transport)
} }
_ => None, _ => 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 { fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right) crate::ai_pipeline::legacy_openai_format_alias_matches(left, 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(),
}
} }
#[allow(clippy::too_many_arguments)] #[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 { fn api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right) crate::ai_pipeline::legacy_openai_format_alias_matches(left, 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(),
}
} }
pub(crate) async fn mark_skipped_local_openai_responses_candidate( pub(crate) async fn mark_skipped_local_openai_responses_candidate(
state: &AppState, 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 { fn is_openai_responses_client_format(client_api_format: &str) -> bool {
let normalized = client_api_format.trim().to_ascii_lowercase(); crate::ai_pipeline::is_openai_responses_family_format(client_api_format)
matches!(
normalized.as_str(),
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact"
)
} }
fn diagnose_same_format_provider_request_body_failure( 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, 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, 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, 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, is_openai_responses_family_format, is_openai_responses_format, kiro_crc32,
map_openai_reasoning_effort_to_gemini_budget, 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_chat_cross_format_sync_product_from_normalized_payload,
maybe_build_openai_responses_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, 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_same_format_sync_body_from_normalized_payload,
maybe_build_standard_sync_finalize_product_from_normalized_payload, maybe_build_standard_sync_finalize_product_from_normalized_payload,
normalize_claude_request_to_openai_chat_request, 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_openai_responses_request_to_openai_chat_request,
normalize_provider_private_report_context, normalize_provider_private_response_value, normalize_provider_private_report_context, normalize_provider_private_response_value,
normalize_standard_request_to_openai_chat_request, parse_direct_request_body, 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> { 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:chat" => Some("openai:chat"),
"openai:responses" | "openai:cli" => Some("openai:responses"), "openai:responses" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"), "openai:responses:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"), "openai:image" => Some("openai:image"),
"openai:video" => Some("openai:video"), "openai:video" => Some("openai:video"),
_ => None, _ => 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> { 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" | "openai:chat" => Some("/v1/chat/completions"),
"openai:responses" | "openai:cli" => Some("/v1/responses"), "openai:responses" => Some("/v1/responses"),
"openai:responses:compact" | "openai:compact" => Some("/v1/responses/compact"), "openai:responses:compact" => Some("/v1/responses/compact"),
"openai:image" => Some("/v1/images/generations"), "openai:image" => Some("/v1/images/generations"),
"openai:video" => Some("/v1/videos"), "openai:video" => Some("/v1/videos"),
_ => None, _ => 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 { fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { 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() .as_deref()
.map(str::trim) .map(str::trim)
.unwrap_or_default() .unwrap_or_default()
.to_ascii_lowercase(); .to_string();
if !matches!( if !crate::ai_pipeline::is_openai_responses_family_format(&provider_api_format) {
provider_api_format.as_str(),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
return false; 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 { pub(crate) fn should_bypass_execution_runtime_plan(plan: &ExecutionPlan) -> bool {
let provider_api_format = plan.provider_api_format.trim().to_ascii_lowercase(); if !crate::ai_pipeline::is_openai_responses_family_format(&plan.provider_api_format) {
if !matches!(
provider_api_format.as_str(),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact"
) {
return false; return false;
} }

View File

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

View File

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

View File

@@ -289,11 +289,7 @@ fn provider_query_key_supports_endpoint(
} }
fn provider_query_normalize_api_format_alias(value: &str) -> String { fn provider_query_normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn provider_query_transport_supports_standard_test_execution( fn provider_query_transport_supports_standard_test_execution(
@@ -301,11 +297,11 @@ fn provider_query_transport_supports_standard_test_execution(
transport: &AdminGatewayProviderTransportSnapshot, transport: &AdminGatewayProviderTransportSnapshot,
api_format: &str, api_format: &str,
) -> bool { ) -> bool {
match api_format { match crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str() {
"openai:chat" => { "openai:chat" => {
crate::provider_transport::policy::supports_local_openai_chat_transport(transport) 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( crate::provider_transport::policy::supports_local_standard_transport_with_network(
transport, api_format, 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_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" => { "openai:chat" => {
let Some(mut provider_request_body) = let Some(mut provider_request_body) =
crate::ai_pipeline::build_local_openai_chat_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( crate::ai_pipeline::build_cross_format_openai_chat_request_body(
&request_body, &request_body,
&candidate.effective_model, &candidate.effective_model,
provider_api_format, normalized_provider_api_format.as_str(),
false, false,
) )
else { else {
@@ -985,12 +983,12 @@ async fn provider_query_execute_standard_test_candidate(
} }
provider_request_body provider_request_body
} }
"openai:responses" | "openai:cli" => { "openai:responses" => {
let Some(mut provider_request_body) = let Some(mut provider_request_body) =
crate::ai_pipeline::build_cross_format_openai_chat_request_body( crate::ai_pipeline::build_cross_format_openai_chat_request_body(
&request_body, &request_body,
&candidate.effective_model, &candidate.effective_model,
provider_api_format, normalized_provider_api_format.as_str(),
false, false,
) )
else { else {
@@ -1037,13 +1035,19 @@ async fn provider_query_execute_standard_test_candidate(
} else { } else {
None None
}; };
let oauth_auth = match provider_api_format { let oauth_auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(
"openai:chat" | "openai:responses" | "openai:cli" | "claude:chat" | "claude:cli" provider_api_format,
| "gemini:chat" | "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?, )
.as_str()
{
"openai:chat" | "openai:responses" | "claude:chat" | "claude:cli" | "gemini:chat"
| "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
_ => None, _ => None,
}; };
let auth = match provider_api_format { let auth = match crate::ai_pipeline::normalize_legacy_openai_format_alias(provider_api_format)
"openai:chat" | "openai:responses" | "openai:cli" => { .as_str()
{
"openai:chat" | "openai:responses" => {
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport) crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
.or(oauth_auth) .or(oauth_auth)
} }
@@ -1104,7 +1108,7 @@ async fn provider_query_execute_standard_test_candidate(
Some("application/json"), Some("application/json"),
) )
} }
"openai:responses" | "openai:cli" => { "openai:responses" => {
crate::provider_transport::auth::build_complete_passthrough_headers_with_auth( crate::provider_transport::auth::build_complete_passthrough_headers_with_auth(
&parts.headers, &parts.headers,
auth_header.as_deref().unwrap_or_default(), auth_header.as_deref().unwrap_or_default(),
@@ -1158,10 +1162,7 @@ async fn provider_query_execute_standard_test_candidate(
response_body: None, response_body: None,
}); });
} }
if matches!( if crate::ai_pipeline::is_openai_responses_format(provider_api_format) {
provider_api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
) {
crate::ai_pipeline::apply_codex_openai_responses_special_headers( crate::ai_pipeline::apply_codex_openai_responses_special_headers(
&mut request_headers, &mut request_headers,
&provider_request_body, &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 { fn provider_query_supports_cli_standard_test_api_format(api_format: &str) -> bool {
matches!( matches!(
api_format, crate::ai_pipeline::normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:responses" | "openai:cli" | "claude:cli" | "gemini:cli" "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 { fn normalize_api_format_alias(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { 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(); let provider_type = provider.provider_type.trim().to_ascii_lowercase();
if provider_type == "codex" if provider_type == "codex"
&& matches!( && crate::ai_pipeline::is_openai_responses_format(&existing_endpoint.api_format)
existing_endpoint
.api_format
.trim()
.to_ascii_lowercase()
.as_str(),
"openai:responses" | "openai:cli"
)
{ {
let has_config_in_payload = fields.contains("config"); let has_config_in_payload = fields.contains("config");
let config_payload = if has_config_in_payload { let config_payload = if has_config_in_payload {

View File

@@ -458,15 +458,13 @@ fn aggregate_sync_sse_response_for_client(
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.filter(|value| !value.is_empty()); .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(value) if value.eq_ignore_ascii_case("openai:chat") => {
aggregate_openai_chat_stream_sync_response(body) aggregate_openai_chat_stream_sync_response(body)
} }
Some(value) Some(value)
if value.eq_ignore_ascii_case("openai:responses") if value.eq_ignore_ascii_case("openai:responses")
|| value.eq_ignore_ascii_case("openai:responses:compact") || value.eq_ignore_ascii_case("openai:responses:compact") =>
|| value.eq_ignore_ascii_case("openai:cli")
|| value.eq_ignore_ascii_case("openai:compact") =>
{ {
aggregate_openai_responses_stream_sync_response(body) aggregate_openai_responses_stream_sync_response(body)
} }
@@ -531,18 +529,10 @@ fn resolve_affinity_forward_client_api_format(
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.filter(|value| !value.is_empty()); .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:chat") => Some("openai:chat"),
Some(value) Some(value) if value.eq_ignore_ascii_case("openai:responses") => Some("openai:responses"),
if value.eq_ignore_ascii_case("openai:responses") Some(value) if value.eq_ignore_ascii_case("openai:responses:compact") => {
|| 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("openai:responses:compact") Some("openai:responses:compact")
} }
Some(value) if value.eq_ignore_ascii_case("claude:chat") => Some("claude:chat"), 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()) .and_then(|decision| decision.auth_endpoint_signature.as_deref())
.map(str::trim) .map(str::trim)
.filter(|signature| !signature.is_empty())?; .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:chat" => Some("openai:chat"),
"openai:responses" | "openai:cli" => Some("openai:responses"), "openai:responses" => Some("openai:responses"),
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"), "openai:responses:compact" => Some("openai:responses:compact"),
"openai:image" => Some("openai:image"), "openai:image" => Some("openai:image"),
"claude:chat" => Some("claude:chat"), "claude:chat" => Some("claude:chat"),
"gemini:chat" => Some("gemini: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] { 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:chat"
| "openai:responses" | "openai:responses"
| "openai:responses:compact" | "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "claude:chat" | "claude:chat"
| "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS, | "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
"openai:image" => &["openai:image"], "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()) .or(item.endpoint_api_format.as_deref())
.map(str::trim) .map(str::trim)
.filter(|value| !value.is_empty()); .filter(|value| !value.is_empty());
let Some(value) = api_format else {
return false;
};
matches!( matches!(
api_format, crate::ai_pipeline::normalize_legacy_openai_format_alias(value).as_str(),
Some(value) "openai:chat"
if value.eq_ignore_ascii_case("openai:chat") | "openai:responses"
|| value.eq_ignore_ascii_case("openai:responses") | "openai:responses:compact"
|| value.eq_ignore_ascii_case("openai:responses:compact") | "openai:image"
|| value.eq_ignore_ascii_case("openai:cli") | "claude:chat"
|| value.eq_ignore_ascii_case("openai:compact") | "claude:cli"
|| value.eq_ignore_ascii_case("openai:image")
|| value.eq_ignore_ascii_case("claude:chat")
|| value.eq_ignore_ascii_case("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}"
);
}
}

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Shared admin contracts and pure helpers for Aether" description = "Shared admin contracts and pure helpers for Aether"
[dependencies] [dependencies]
aether-ai-formats.workspace = true
aether-billing.workspace = true aether-billing.workspace = true
aether-contracts.workspace = true aether-contracts.workspace = true
aether-data.workspace = true aether-data.workspace = true

View File

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

View File

@@ -86,9 +86,50 @@ impl FromStr for FormatId {
} }
} }
pub fn normalize_legacy_openai_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(),
}
}
pub fn legacy_openai_format_alias_matches(left: &str, right: &str) -> bool {
normalize_legacy_openai_format_alias(left) == normalize_legacy_openai_format_alias(right)
}
pub fn openai_format_storage_aliases(value: &str) -> Vec<String> {
match normalize_legacy_openai_format_alias(value).as_str() {
"openai:responses" => vec!["openai:responses".to_string(), "openai:cli".to_string()],
"openai:responses:compact" => vec![
"openai:responses:compact".to_string(),
"openai:compact".to_string(),
],
normalized => vec![normalized.to_string()],
}
}
pub fn is_openai_responses_format(value: &str) -> bool {
normalize_legacy_openai_format_alias(value) == "openai:responses"
}
pub fn is_openai_responses_compact_format(value: &str) -> bool {
normalize_legacy_openai_format_alias(value) == "openai:responses:compact"
}
pub fn is_openai_responses_family_format(value: &str) -> bool {
matches!(
normalize_legacy_openai_format_alias(value).as_str(),
"openai:responses" | "openai:responses:compact"
)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::FormatId; use super::{
legacy_openai_format_alias_matches, normalize_legacy_openai_format_alias,
openai_format_storage_aliases, FormatId,
};
#[test] #[test]
fn normalizes_legacy_aliases() { fn normalizes_legacy_aliases() {
@@ -109,4 +150,43 @@ mod tests {
Some(FormatId::GeminiGenerateContent) Some(FormatId::GeminiGenerateContent)
); );
} }
#[test]
fn normalizes_openai_legacy_aliases_without_rewriting_other_families() {
assert_eq!(
normalize_legacy_openai_format_alias(" openai:cli "),
"openai:responses"
);
assert_eq!(
normalize_legacy_openai_format_alias("OPENAI:COMPACT"),
"openai:responses:compact"
);
assert_eq!(
normalize_legacy_openai_format_alias("claude:cli"),
"claude:cli"
);
assert!(legacy_openai_format_alias_matches(
"openai:cli",
"openai:responses"
));
}
#[test]
fn storage_aliases_include_legacy_openai_rows_only() {
assert_eq!(
openai_format_storage_aliases("openai:responses"),
vec!["openai:responses".to_string(), "openai:cli".to_string()]
);
assert_eq!(
openai_format_storage_aliases("openai:compact"),
vec![
"openai:responses:compact".to_string(),
"openai:compact".to_string()
]
);
assert_eq!(
openai_format_storage_aliases("claude:cli"),
vec!["claude:cli".to_string()]
);
}
} }

View File

@@ -22,7 +22,12 @@ pub use canonical::{
CanonicalRole, CanonicalStopReason, CanonicalStreamEvent, CanonicalStreamFrame, CanonicalRole, CanonicalStopReason, CanonicalStreamEvent, CanonicalStreamFrame,
CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition, CanonicalUsage, CanonicalThinkingConfig, CanonicalToolChoice, CanonicalToolDefinition, CanonicalUsage,
}; };
pub use formats::{FormatFamily, FormatId, FormatProfile}; pub use formats::{
is_openai_responses_compact_format, is_openai_responses_family_format,
is_openai_responses_format, legacy_openai_format_alias_matches,
normalize_legacy_openai_format_alias, openai_format_storage_aliases, FormatFamily, FormatId,
FormatProfile,
};
pub use registry::{ pub use registry::{
build_stream_transcoder, convert_request, convert_response, FormatContext, FormatError, build_stream_transcoder, convert_request, convert_response, FormatContext, FormatError,
}; };

View File

@@ -189,3 +189,8 @@ pub use crate::planner::standard::{
CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_MODEL, CODEX_OPENAI_IMAGE_DEFAULT_VARIATION_PROMPT,
CODEX_OPENAI_IMAGE_INTERNAL_MODEL, CODEX_OPENAI_IMAGE_INTERNAL_MODEL,
}; };
pub use aether_ai_formats::{
is_openai_responses_compact_format, is_openai_responses_family_format,
is_openai_responses_format, legacy_openai_format_alias_matches,
normalize_legacy_openai_format_alias, openai_format_storage_aliases,
};

View File

@@ -37,12 +37,8 @@ pub fn build_core_error_body_for_client_format(
let mut error_object = Map::new(); let mut error_object = Map::new();
error_object.insert("message".to_string(), Value::String(message.to_string())); error_object.insert("message".to_string(), Value::String(message.to_string()));
match client_api_format.trim().to_ascii_lowercase().as_str() { match aether_ai_formats::normalize_legacy_openai_format_alias(client_api_format).as_str() {
"openai:chat" "openai:chat" | "openai:responses" | "openai:responses:compact" => {
| "openai:responses"
| "openai:cli"
| "openai:compact"
| "openai:responses:compact" => {
error_object.insert( error_object.insert(
"type".to_string(), "type".to_string(),
Value::String(map_local_sync_error_kind_to_openai_type(kind).to_string()), Value::String(map_local_sync_error_kind_to_openai_type(kind).to_string()),

View File

@@ -1,5 +1,9 @@
#![allow(dead_code)] #![allow(dead_code)]
use aether_ai_formats::{
is_openai_responses_compact_format, legacy_openai_format_alias_matches,
normalize_legacy_openai_format_alias,
};
use aether_provider_transport::auth::{ use aether_provider_transport::auth::{
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth, resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
}; };
@@ -52,27 +56,18 @@ pub fn request_candidate_api_format_preference(
client_api_format: &str, client_api_format: &str,
provider_api_format: &str, provider_api_format: &str,
) -> Option<(u8, u8)> { ) -> Option<(u8, u8)> {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
if matches!( if client_api_format == "openai:responses:compact" {
client_api_format.as_str(), return (provider_api_format == "openai:responses:compact").then_some((0, 0));
"openai:compact" | "openai:responses:compact"
) {
return matches!(
provider_api_format.as_str(),
"openai:compact" | "openai:responses:compact"
)
.then_some((0, 0));
} }
let (client_family, client_kind) = let (client_family, client_kind) =
parse_non_compact_standard_api_format(client_api_format.as_str())?; parse_non_compact_standard_api_format(client_api_format.as_str())?;
let (provider_family, provider_kind) = let (provider_family, provider_kind) =
parse_non_compact_standard_api_format(provider_api_format.as_str())?; parse_non_compact_standard_api_format(provider_api_format.as_str())?;
let preference_bucket = if canonical_standard_api_format(client_api_format.as_str()) let preference_bucket = if client_api_format == provider_api_format {
== canonical_standard_api_format(provider_api_format.as_str())
{
0 0
} else if client_kind == provider_kind { } else if client_kind == provider_kind {
1 1
@@ -92,11 +87,8 @@ pub fn request_candidate_api_formats(
client_api_format: &str, client_api_format: &str,
_require_streaming: bool, _require_streaming: bool,
) -> Vec<&'static str> { ) -> Vec<&'static str> {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
if matches!( if client_api_format == "openai:responses:compact" {
client_api_format.as_str(),
"openai:compact" | "openai:responses:compact"
) {
return vec!["openai:responses:compact"]; return vec!["openai:responses:compact"];
} }
if parse_non_compact_standard_api_format(client_api_format.as_str()).is_none() { if parse_non_compact_standard_api_format(client_api_format.as_str()).is_none() {
@@ -115,33 +107,25 @@ pub fn request_conversion_kind(
client_api_format: &str, client_api_format: &str,
provider_api_format: &str, provider_api_format: &str,
) -> Option<RequestConversionKind> { ) -> Option<RequestConversionKind> {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
if client_api_format == provider_api_format { if client_api_format == provider_api_format {
return None; return None;
} }
if normalized_same_standard_api_format(client_api_format.as_str(), provider_api_format.as_str())
{
return None;
}
if !is_standard_api_format(client_api_format.as_str()) if !is_standard_api_format(client_api_format.as_str())
|| !is_standard_api_format(provider_api_format.as_str()) || !is_standard_api_format(provider_api_format.as_str())
{ {
return None; return None;
} }
if matches!( if is_openai_responses_compact_format(client_api_format.as_str())
client_api_format.as_str(), || is_openai_responses_compact_format(provider_api_format.as_str())
"openai:compact" | "openai:responses:compact" {
) || matches!(
provider_api_format.as_str(),
"openai:compact" | "openai:responses:compact"
) {
return None; return None;
} }
match provider_api_format.as_str() { match provider_api_format.as_str() {
"openai:chat" => Some(RequestConversionKind::ToOpenAIChat), "openai:chat" => Some(RequestConversionKind::ToOpenAIChat),
"openai:responses" | "openai:cli" => Some(RequestConversionKind::ToOpenAiResponses), "openai:responses" => Some(RequestConversionKind::ToOpenAiResponses),
"claude:chat" | "claude:cli" => Some(RequestConversionKind::ToClaudeStandard), "claude:chat" | "claude:cli" => Some(RequestConversionKind::ToClaudeStandard),
"gemini:chat" | "gemini:cli" => Some(RequestConversionKind::ToGeminiStandard), "gemini:chat" | "gemini:cli" => Some(RequestConversionKind::ToGeminiStandard),
_ => None, _ => None,
@@ -152,15 +136,11 @@ pub fn sync_chat_response_conversion_kind(
provider_api_format: &str, provider_api_format: &str,
client_api_format: &str, client_api_format: &str,
) -> Option<SyncChatResponseConversionKind> { ) -> Option<SyncChatResponseConversionKind> {
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
if provider_api_format == client_api_format { if provider_api_format == client_api_format {
return None; return None;
} }
if normalized_same_standard_api_format(provider_api_format.as_str(), client_api_format.as_str())
{
return None;
}
if !is_standard_api_format(provider_api_format.as_str()) { if !is_standard_api_format(provider_api_format.as_str()) {
return None; return None;
} }
@@ -177,26 +157,19 @@ pub fn sync_cli_response_conversion_kind(
provider_api_format: &str, provider_api_format: &str,
client_api_format: &str, client_api_format: &str,
) -> Option<SyncCliResponseConversionKind> { ) -> Option<SyncCliResponseConversionKind> {
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
if provider_api_format == client_api_format { if provider_api_format == client_api_format {
return None; return None;
} }
if normalized_same_standard_api_format(provider_api_format.as_str(), client_api_format.as_str())
{
return None;
}
if !is_standard_api_format(provider_api_format.as_str()) { if !is_standard_api_format(provider_api_format.as_str()) {
return None; return None;
} }
if !matches!( if !is_openai_responses_compact_format(client_api_format.as_str()) {
client_api_format.as_str(),
"openai:compact" | "openai:responses:compact"
) {
request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str())?; request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str())?;
} }
match client_api_format.as_str() { match client_api_format.as_str() {
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact" => { "openai:responses" | "openai:responses:compact" => {
Some(SyncCliResponseConversionKind::ToOpenAiResponses) Some(SyncCliResponseConversionKind::ToOpenAiResponses)
} }
"claude:cli" => Some(SyncCliResponseConversionKind::ToClaudeCli), "claude:cli" => Some(SyncCliResponseConversionKind::ToClaudeCli),
@@ -209,8 +182,8 @@ pub fn request_conversion_requires_enable_flag(
client_api_format: &str, client_api_format: &str,
provider_api_format: &str, provider_api_format: &str,
) -> bool { ) -> bool {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
match ( match (
api_data_format_id(client_api_format.as_str()), api_data_format_id(client_api_format.as_str()),
api_data_format_id(provider_api_format.as_str()), api_data_format_id(provider_api_format.as_str()),
@@ -227,8 +200,8 @@ pub fn request_conversion_enabled_for_transport(
client_api_format: &str, client_api_format: &str,
provider_api_format: &str, provider_api_format: &str,
) -> bool { ) -> bool {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
if client_api_format == provider_api_format { if client_api_format == provider_api_format {
return true; return true;
} }
@@ -250,8 +223,8 @@ pub fn request_pair_allowed_for_transport(
client_api_format: &str, client_api_format: &str,
provider_api_format: &str, provider_api_format: &str,
) -> bool { ) -> bool {
let client_api_format = client_api_format.trim().to_ascii_lowercase(); let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = provider_api_format.trim().to_ascii_lowercase(); let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
if client_api_format == provider_api_format { if client_api_format == provider_api_format {
return true; return true;
} }
@@ -304,15 +277,9 @@ pub fn request_conversion_transport_unsupported_reason(
return local_kiro_request_transport_unsupported_reason_with_network(transport); return local_kiro_request_transport_unsupported_reason_with_network(transport);
} }
match transport match normalize_legacy_openai_format_alias(&transport.endpoint.api_format).as_str() {
.endpoint
.api_format
.trim()
.to_ascii_lowercase()
.as_str()
{
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport), "openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
"openai:responses" | "openai:cli" | "openai:responses:compact" | "openai:compact" => { "openai:responses" | "openai:responses:compact" => {
local_standard_transport_unsupported_reason_with_network( local_standard_transport_unsupported_reason_with_network(
transport, transport,
transport.endpoint.api_format.trim(), transport.endpoint.api_format.trim(),
@@ -341,18 +308,10 @@ pub fn request_conversion_direct_auth(
transport: &GatewayProviderTransportSnapshot, transport: &GatewayProviderTransportSnapshot,
_kind: RequestConversionKind, _kind: RequestConversionKind,
) -> Option<(String, String)> { ) -> Option<(String, String)> {
match transport match normalize_legacy_openai_format_alias(&transport.endpoint.api_format).as_str() {
.endpoint "openai:chat" | "openai:responses" | "openai:responses:compact" => {
.api_format resolve_local_openai_bearer_auth(transport)
.trim() }
.to_ascii_lowercase()
.as_str()
{
"openai:chat"
| "openai:responses"
| "openai:cli"
| "openai:compact"
| "openai:responses:compact" => resolve_local_openai_bearer_auth(transport),
"gemini:chat" | "gemini:cli" => { "gemini:chat" | "gemini:cli" => {
if is_vertex_api_key_transport_context(transport) { if is_vertex_api_key_transport_context(transport) {
resolve_local_vertex_api_key_query_auth(transport) resolve_local_vertex_api_key_query_auth(transport)
@@ -368,11 +327,9 @@ pub fn request_conversion_direct_auth(
fn is_standard_api_format(api_format: &str) -> bool { fn is_standard_api_format(api_format: &str) -> bool {
matches!( matches!(
api_format, normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:chat" "openai:chat"
| "openai:responses" | "openai:responses"
| "openai:cli"
| "openai:compact"
| "openai:responses:compact" | "openai:responses:compact"
| "claude:chat" | "claude:chat"
| "claude:cli" | "claude:cli"
@@ -381,15 +338,16 @@ fn is_standard_api_format(api_format: &str) -> bool {
) )
} }
fn parse_non_compact_standard_api_format(api_format: &str) -> Option<(&str, &str)> { fn parse_non_compact_standard_api_format(api_format: &str) -> Option<(&'static str, &'static str)> {
let (family, kind) = api_format.split_once(':')?; match normalize_legacy_openai_format_alias(api_format).as_str() {
if family == "openai" && kind == "responses" { "openai:chat" => Some(("openai", "chat")),
return Some((family, "cli")); "openai:responses" => Some(("openai", "cli")),
"claude:chat" => Some(("claude", "chat")),
"claude:cli" => Some(("claude", "cli")),
"gemini:chat" => Some(("gemini", "chat")),
"gemini:cli" => Some(("gemini", "cli")),
_ => None,
} }
if !STANDARD_API_FAMILY_ORDER.contains(&family) || !matches!(kind, "chat" | "cli") {
return None;
}
Some((family, kind))
} }
fn standard_api_family_priority(family: &str) -> u8 { fn standard_api_family_priority(family: &str) -> u8 {
@@ -400,33 +358,17 @@ fn standard_api_family_priority(family: &str) -> u8 {
} }
fn api_data_format_id(api_format: &str) -> Option<&'static str> { fn api_data_format_id(api_format: &str) -> Option<&'static str> {
match api_format { match normalize_legacy_openai_format_alias(api_format).as_str() {
"claude:chat" | "claude:cli" => Some("claude"), "claude:chat" | "claude:cli" => Some("claude"),
"gemini:chat" | "gemini:cli" => Some("gemini"), "gemini:chat" | "gemini:cli" => Some("gemini"),
"openai:chat" => Some("openai_chat"), "openai:chat" => Some("openai_chat"),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact" => { "openai:responses" | "openai:responses:compact" => Some("openai_responses"),
Some("openai_responses")
}
_ => None, _ => None,
} }
} }
fn normalized_same_standard_api_format(left: &str, right: &str) -> bool { fn normalized_same_standard_api_format(left: &str, right: &str) -> bool {
matches!( legacy_openai_format_alias_matches(left, right)
(left, right),
("openai:responses", "openai:cli")
| ("openai:cli", "openai:responses")
| ("openai:responses:compact", "openai:compact")
| ("openai:compact", "openai:responses:compact")
)
}
fn canonical_standard_api_format(api_format: &str) -> &str {
match api_format {
"openai:cli" => "openai:responses",
"openai:compact" => "openai:responses:compact",
other => other,
}
} }
fn endpoint_accepts_client_api_format( fn endpoint_accepts_client_api_format(

View File

@@ -83,12 +83,10 @@ fn supports_standard_stream_rewrite(provider_api_format: &str, client_api_format
fn is_standard_provider_api_format(api_format: &str) -> bool { fn is_standard_provider_api_format(api_format: &str) -> bool {
matches!( matches!(
api_format, aether_ai_formats::normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:chat" "openai:chat"
| "openai:responses" | "openai:responses"
| "openai:responses:compact" | "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "claude:chat" | "claude:chat"
| "claude:cli" | "claude:cli"
| "gemini:chat" | "gemini:chat"
@@ -102,13 +100,8 @@ fn is_standard_chat_client_api_format(api_format: &str) -> bool {
fn is_standard_cli_client_api_format(api_format: &str) -> bool { fn is_standard_cli_client_api_format(api_format: &str) -> bool {
matches!( matches!(
api_format, aether_ai_formats::normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:responses" "openai:responses" | "openai:responses:compact" | "claude:cli" | "gemini:cli"
| "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "claude:cli"
| "gemini:cli"
) )
} }

View File

@@ -622,9 +622,9 @@ pub fn aggregate_standard_chat_stream_sync_response(
body: &[u8], body: &[u8],
provider_api_format: &str, provider_api_format: &str,
) -> Option<Value> { ) -> Option<Value> {
match provider_api_format.trim().to_ascii_lowercase().as_str() { match aether_ai_formats::normalize_legacy_openai_format_alias(provider_api_format).as_str() {
"openai:chat" => aggregate_openai_chat_stream_sync_response(body), "openai:chat" => aggregate_openai_chat_stream_sync_response(body),
"openai:responses" | "openai:cli" | "openai:compact" | "openai:responses:compact" => { "openai:responses" | "openai:responses:compact" => {
aggregate_openai_responses_stream_sync_response(body) aggregate_openai_responses_stream_sync_response(body)
} }
"claude:chat" | "claude:cli" => aggregate_claude_stream_sync_response(body), "claude:chat" | "claude:cli" => aggregate_claude_stream_sync_response(body),
@@ -1255,11 +1255,7 @@ fn is_openai_responses_family_api_format(api_format: &str) -> bool {
} }
fn normalize_openai_responses_family_api_format(api_format: &str) -> String { fn normalize_openai_responses_family_api_format(api_format: &str) -> String {
match api_format.trim().to_ascii_lowercase().as_str() { aether_ai_formats::normalize_legacy_openai_format_alias(api_format)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn parse_stream_json_events(body: &[u8]) -> Option<Vec<Value>> { fn parse_stream_json_events(body: &[u8]) -> Option<Vec<Value>> {

View File

@@ -26,10 +26,7 @@ pub fn force_upstream_streaming_for_provider(
provider_api_format: &str, provider_api_format: &str,
) -> bool { ) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex") provider_type.trim().eq_ignore_ascii_case("codex")
&& matches!( && aether_ai_formats::is_openai_responses_format(provider_api_format)
provider_api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses" | "openai:cli"
)
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -27,21 +27,12 @@ const UUID_NAMESPACE_OID_BYTES: [u8; 16] = [
fn is_codex_openai_responses_request(provider_type: &str, provider_api_format: &str) -> bool { fn is_codex_openai_responses_request(provider_type: &str, provider_api_format: &str) -> bool {
provider_type.trim().eq_ignore_ascii_case("codex") provider_type.trim().eq_ignore_ascii_case("codex")
&& matches!( && (aether_ai_formats::is_openai_responses_family_format(provider_api_format)
provider_api_format.trim().to_ascii_lowercase().as_str(), || is_openai_image_request(provider_api_format))
"openai:responses"
| "openai:responses:compact"
| "openai:cli"
| "openai:compact"
| "openai:image"
)
} }
fn is_openai_responses_compact_request(provider_api_format: &str) -> bool { fn is_openai_responses_compact_request(provider_api_format: &str) -> bool {
matches!( aether_ai_formats::is_openai_responses_compact_format(provider_api_format)
provider_api_format.trim().to_ascii_lowercase().as_str(),
"openai:responses:compact" | "openai:compact"
)
} }
fn is_openai_image_request(provider_api_format: &str) -> bool { fn is_openai_image_request(provider_api_format: &str) -> bool {
@@ -413,10 +404,8 @@ pub fn apply_codex_openai_responses_special_headers(
} }
} }
if matches!( if aether_ai_formats::is_openai_responses_format(provider_api_format)
provider_api_format.trim().to_ascii_lowercase().as_str(), && !header_map_has_non_empty_value(original_headers, "conversation_id")
"openai:responses" | "openai:cli"
) && !header_map_has_non_empty_value(original_headers, "conversation_id")
&& !btree_map_has_non_empty_value(provider_request_headers, "conversation_id") && !btree_map_has_non_empty_value(provider_request_headers, "conversation_id")
{ {
if let Some(short_session_id) = short_session_id.as_deref() { if let Some(short_session_id) = short_session_id.as_deref() {

View File

@@ -67,28 +67,24 @@ pub fn build_standard_request_body_from_canonical(
provider_api_format: &str, provider_api_format: &str,
upstream_is_stream: bool, upstream_is_stream: bool,
) -> Option<Value> { ) -> Option<Value> {
match provider_api_format.trim().to_ascii_lowercase().as_str() { match aether_ai_formats::normalize_legacy_openai_format_alias(provider_api_format).as_str() {
"openai:chat" => build_local_openai_chat_request_body( "openai:chat" => build_local_openai_chat_request_body(
canonical_request, canonical_request,
mapped_model, mapped_model,
upstream_is_stream, upstream_is_stream,
), ),
"openai:responses" | "openai:cli" => { "openai:responses" => convert_openai_chat_request_to_openai_responses_request(
convert_openai_chat_request_to_openai_responses_request( canonical_request,
canonical_request, mapped_model,
mapped_model, upstream_is_stream,
upstream_is_stream, false,
false, ),
) "openai:responses:compact" => convert_openai_chat_request_to_openai_responses_request(
} canonical_request,
"openai:responses:compact" | "openai:compact" => { mapped_model,
convert_openai_chat_request_to_openai_responses_request( false,
canonical_request, true,
mapped_model, ),
false,
true,
)
}
"claude:chat" | "claude:cli" => convert_openai_chat_request_to_claude_request( "claude:chat" | "claude:cli" => convert_openai_chat_request_to_claude_request(
canonical_request, canonical_request,
mapped_model, mapped_model,
@@ -121,9 +117,9 @@ fn normalize_standard_request_to_openai_chat_request_cow<'a>(
client_api_format: &str, client_api_format: &str,
request_path: &str, request_path: &str,
) -> Option<Cow<'a, Value>> { ) -> Option<Cow<'a, Value>> {
match client_api_format.trim().to_ascii_lowercase().as_str() { match aether_ai_formats::normalize_legacy_openai_format_alias(client_api_format).as_str() {
"openai:chat" => Some(Cow::Borrowed(body_json)), "openai:chat" => Some(Cow::Borrowed(body_json)),
"openai:responses" | "openai:cli" | "openai:responses:compact" | "openai:compact" => { "openai:responses" | "openai:responses:compact" => {
normalize_openai_responses_request_to_openai_chat_request(body_json).map(Cow::Owned) normalize_openai_responses_request_to_openai_chat_request(body_json).map(Cow::Owned)
} }
"claude:chat" | "claude:cli" => { "claude:chat" | "claude:cli" => {

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Shared data contracts and repository traits for Aether Rust services" description = "Shared data contracts and repository traits for Aether Rust services"
[dependencies] [dependencies]
aether-ai-formats.workspace = true
async-trait.workspace = true async-trait.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true

View File

@@ -58,11 +58,7 @@ impl StoredMinimalCandidateSelectionRow {
} }
fn normalize_api_format(value: &str) -> String { fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { aether_ai_formats::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Shared data access contracts and config for Aether Rust services" description = "Shared data access contracts and config for Aether Rust services"
[dependencies] [dependencies]
aether-ai-formats.workspace = true
aether-data-contracts.workspace = true aether-data-contracts.workspace = true
aether-cache.workspace = true aether-cache.workspace = true
aether-wallet.workspace = true aether-wallet.workspace = true

View File

@@ -70,11 +70,7 @@ impl MinimalCandidateSelectionReadRepository for InMemoryMinimalCandidateSelecti
} }
fn normalize_api_format(value: &str) -> String { fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { aether_ai_formats::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn api_format_matches(left: &str, right: &str) -> bool { fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -264,14 +264,8 @@ impl SqlxMinimalCandidateSelectionReadRepository {
} }
} }
fn api_format_aliases(api_format: &str) -> Vec<&str> { fn api_format_aliases(api_format: &str) -> Vec<String> {
match api_format.trim().to_ascii_lowercase().as_str() { aether_ai_formats::openai_format_storage_aliases(api_format)
"openai:responses" => vec!["openai:responses", "openai:cli"],
"openai:cli" => vec!["openai:responses", "openai:cli"],
"openai:responses:compact" => vec!["openai:responses:compact", "openai:compact"],
"openai:compact" => vec!["openai:responses:compact", "openai:compact"],
_ => vec![api_format],
}
} }
fn dedupe_candidate_selection_rows( fn dedupe_candidate_selection_rows(

View File

@@ -6,6 +6,7 @@ license.workspace = true
repository.workspace = true repository.workspace = true
[dependencies] [dependencies]
aether-ai-formats.workspace = true
aether-contracts.workspace = true aether-contracts.workspace = true
aether-data-contracts.workspace = true aether-data-contracts.workspace = true
aether-provider-transport.workspace = true aether-provider-transport.workspace = true

View File

@@ -570,11 +570,7 @@ fn wildcard_matches(pattern: &str, model_id: &str) -> bool {
} }
fn normalize_api_format(value: &str) -> String { fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { aether_ai_formats::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -381,14 +381,12 @@ fn apply_fetch_header_rules(
} }
fn standard_models_fetch_headers(api_format: &str) -> BTreeMap<String, String> { fn standard_models_fetch_headers(api_format: &str) -> BTreeMap<String, String> {
let api_format = api_format.trim().to_ascii_lowercase(); let api_format = aether_ai_formats::normalize_legacy_openai_format_alias(api_format);
match api_format.as_str() { match api_format.as_str() {
"openai:responses" | "openai:responses:compact" | "openai:cli" | "openai:compact" => { "openai:responses" | "openai:responses:compact" => BTreeMap::from([(
BTreeMap::from([( "user-agent".to_string(),
"user-agent".to_string(), OPENAI_RESPONSES_USER_AGENT.to_string(),
OPENAI_RESPONSES_USER_AGENT.to_string(), )]),
)])
}
"claude:chat" => BTreeMap::from([( "claude:chat" => BTreeMap::from([(
"anthropic-version".to_string(), "anthropic-version".to_string(),
CLAUDE_VERSION_HEADER.to_string(), CLAUDE_VERSION_HEADER.to_string(),

View File

@@ -200,13 +200,5 @@ fn local_same_format_transport_unsupported_reason(
} }
fn same_api_format(left: &str, right: &str) -> bool { fn same_api_format(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_format_alias(right) aether_ai_formats::legacy_openai_format_alias_matches(left, 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(),
}
} }

View File

@@ -181,15 +181,11 @@ pub fn fixed_provider_endpoint_template_by_api_format(
provider_type: &str, provider_type: &str,
api_format: &str, api_format: &str,
) -> Option<&'static FixedProviderEndpointTemplate> { ) -> Option<&'static FixedProviderEndpointTemplate> {
let normalized = match api_format.trim().to_ascii_lowercase().as_str() { let normalized = aether_ai_formats::normalize_legacy_openai_format_alias(api_format);
"openai:cli" => "openai:responses",
"openai:compact" => "openai:responses:compact",
_ => api_format.trim(),
};
fixed_provider_template(provider_type)? fixed_provider_template(provider_type)?
.endpoints .endpoints
.iter() .iter()
.find(|item| item.api_format.eq_ignore_ascii_case(normalized)) .find(|item| item.api_format.eq_ignore_ascii_case(&normalized))
} }
pub fn provider_type_supports_model_fetch(provider_type: &str) -> bool { pub fn provider_type_supports_model_fetch(provider_type: &str) -> bool {

View File

@@ -60,17 +60,19 @@ pub fn build_transport_request_url(
)); ));
} }
let url = match provider_api_format.as_str() { let url = match aether_ai_formats::normalize_legacy_openai_format_alias(&provider_api_format)
.as_str()
{
"openai:chat" => Some(build_openai_chat_url( "openai:chat" => Some(build_openai_chat_url(
&transport.endpoint.base_url, &transport.endpoint.base_url,
params.request_query, params.request_query,
)), )),
"openai:responses" | "openai:cli" => Some(build_openai_responses_url( "openai:responses" => Some(build_openai_responses_url(
&transport.endpoint.base_url, &transport.endpoint.base_url,
params.request_query, params.request_query,
false, false,
)), )),
"openai:responses:compact" | "openai:compact" => Some(build_openai_responses_url( "openai:responses:compact" => Some(build_openai_responses_url(
&transport.endpoint.base_url, &transport.endpoint.base_url,
params.request_query, params.request_query,
true, true,

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Pure scheduler health and quota logic extracted from aether-gateway" description = "Pure scheduler health and quota logic extracted from aether-gateway"
[dependencies] [dependencies]
aether-ai-formats.workspace = true
aether-contracts.workspace = true aether-contracts.workspace = true
aether-data-contracts.workspace = true aether-data-contracts.workspace = true
aether-wallet.workspace = true aether-wallet.workspace = true

View File

@@ -253,11 +253,7 @@ pub fn extract_global_priority_for_format(
} }
pub fn normalize_api_format(value: &str) -> String { pub fn normalize_api_format(value: &str) -> String {
match value.trim().to_ascii_lowercase().as_str() { aether_ai_formats::normalize_legacy_openai_format_alias(value)
"openai:cli" => "openai:responses".to_string(),
"openai:compact" => "openai:responses:compact".to_string(),
other => other.to_string(),
}
} }
fn row_has_candidate_model_name( fn row_has_candidate_model_name(

View File

@@ -7,6 +7,7 @@ repository.workspace = true
description = "Usage runtime shared types and queue primitives extracted from aether-gateway" description = "Usage runtime shared types and queue primitives extracted from aether-gateway"
[dependencies] [dependencies]
aether-ai-formats.workspace = true
aether-contracts.workspace = true aether-contracts.workspace = true
aether-data.workspace = true aether-data.workspace = true
aether-data-contracts.workspace = true aether-data-contracts.workspace = true

View File

@@ -121,18 +121,18 @@ pub fn infer_internal_finalize_signature(payload: &GatewaySyncReportRequest) ->
} }
pub fn resolve_internal_finalize_route(signature: &str) -> Option<InternalFinalizeRoute> { pub fn resolve_internal_finalize_route(signature: &str) -> Option<InternalFinalizeRoute> {
match signature { match aether_ai_formats::normalize_legacy_openai_format_alias(signature).as_str() {
"openai:chat" => Some(InternalFinalizeRoute { "openai:chat" => Some(InternalFinalizeRoute {
public_path: "/v1/chat/completions", public_path: "/v1/chat/completions",
route_family: "openai", route_family: "openai",
route_kind: "chat", route_kind: "chat",
}), }),
"openai:responses" | "openai:cli" => Some(InternalFinalizeRoute { "openai:responses" => Some(InternalFinalizeRoute {
public_path: "/v1/responses", public_path: "/v1/responses",
route_family: "openai", route_family: "openai",
route_kind: "responses", route_kind: "responses",
}), }),
"openai:responses:compact" | "openai:compact" => Some(InternalFinalizeRoute { "openai:responses:compact" => Some(InternalFinalizeRoute {
public_path: "/v1/responses/compact", public_path: "/v1/responses/compact",
route_family: "openai", route_family: "openai",
route_kind: "responses:compact", route_kind: "responses:compact",

View File

@@ -694,8 +694,19 @@ const STANDARD_ROUTING_API_FORMATS = [
'gemini:cli' 'gemini:cli'
] ]
function apiDataFormatId(apiFormat: string): string | null { function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
switch (apiFormat.trim().toLowerCase()) { switch (apiFormat.trim().toLowerCase()) {
case 'openai:cli':
return 'openai:responses'
case 'openai:compact':
return 'openai:responses:compact'
default:
return apiFormat.trim().toLowerCase()
}
}
function apiDataFormatId(apiFormat: string): string | null {
switch (normalizeLegacyOpenAIFormatAlias(apiFormat)) {
case 'claude:chat': case 'claude:chat':
case 'claude:cli': case 'claude:cli':
return 'claude' return 'claude'
@@ -706,8 +717,6 @@ function apiDataFormatId(apiFormat: string): string | null {
return 'openai_chat' return 'openai_chat'
case 'openai:responses': case 'openai:responses':
case 'openai:responses:compact': case 'openai:responses:compact':
case 'openai:cli':
case 'openai:compact':
return 'openai_responses' return 'openai_responses'
default: default:
return null return null
@@ -715,8 +724,8 @@ function apiDataFormatId(apiFormat: string): string | null {
} }
function requestConversionKind(clientApiFormat: string, providerApiFormat: string): string | null { function requestConversionKind(clientApiFormat: string, providerApiFormat: string): string | null {
const client = clientApiFormat.trim().toLowerCase() const client = normalizeLegacyOpenAIFormatAlias(clientApiFormat)
const provider = providerApiFormat.trim().toLowerCase() const provider = normalizeLegacyOpenAIFormatAlias(providerApiFormat)
if (client === provider) return null if (client === provider) return null
if (!STANDARD_ROUTING_API_FORMATS.includes(client) || !STANDARD_ROUTING_API_FORMATS.includes(provider)) { if (!STANDARD_ROUTING_API_FORMATS.includes(client) || !STANDARD_ROUTING_API_FORMATS.includes(provider)) {
return null return null
@@ -727,8 +736,6 @@ function requestConversionKind(clientApiFormat: string, providerApiFormat: strin
return 'to_openai_chat' return 'to_openai_chat'
case 'openai:responses': case 'openai:responses':
return 'to_openai_responses' return 'to_openai_responses'
case 'openai:cli':
return 'to_openai_responses'
case 'claude:chat': case 'claude:chat':
case 'claude:cli': case 'claude:cli':
return 'to_claude' return 'to_claude'

View File

@@ -1285,6 +1285,17 @@ function hasDefaultBodyRules(apiFormat: string): boolean {
return (defaultBodyRulesByFormat.value[cacheKey]?.length || 0) > 0 return (defaultBodyRulesByFormat.value[cacheKey]?.length || 0) > 0
} }
function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
switch (apiFormat.trim().toLowerCase()) {
case 'openai:cli':
return 'openai:responses'
case 'openai:compact':
return 'openai:responses:compact'
default:
return apiFormat.trim().toLowerCase()
}
}
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> { async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
if (!apiFormat) return [] if (!apiFormat) return []
const providerType = (props.provider?.provider_type || '').toLowerCase() const providerType = (props.provider?.provider_type || '').toLowerCase()
@@ -1321,22 +1332,23 @@ async function preloadDefaultBodyRules(endpoints: ProviderEndpoint[]): Promise<v
// 获取指定 API 格式的默认路径 // 获取指定 API 格式的默认路径
function getDefaultPath(apiFormat: string, baseUrl?: string): string { function getDefaultPath(apiFormat: string, baseUrl?: string): string {
const providerType = (props.provider?.provider_type || '').toLowerCase() const providerType = (props.provider?.provider_type || '').toLowerCase()
const normalizedApiFormat = normalizeLegacyOpenAIFormatAlias(apiFormat)
if (providerType === 'vertex_ai') { if (providerType === 'vertex_ai') {
if (apiFormat === 'gemini:chat') { if (normalizedApiFormat === 'gemini:chat') {
return '/v1/publishers/google/models/{model}:{action}' return '/v1/publishers/google/models/{model}:{action}'
} }
if (apiFormat === 'claude:chat') { if (normalizedApiFormat === 'claude:chat') {
return '/v1/projects/{project_id}/locations/{region}/publishers/anthropic/models/{model}:{action}' return '/v1/projects/{project_id}/locations/{region}/publishers/anthropic/models/{model}:{action}'
} }
} }
const format = apiFormats.value.find(f => f.value === apiFormat) const format = apiFormats.value.find(f => f.value === normalizedApiFormat)
const defaultPath = format?.default_path || '' const defaultPath = format?.default_path || ''
// Codex 端点使用 /responses 而非 /v1/responses // Codex 端点使用 /responses 而非 /v1/responses
const isCodex = providerType const isCodex = providerType
? providerType === 'codex' ? providerType === 'codex'
: (!!baseUrl && isCodexUrl(baseUrl)) : (!!baseUrl && isCodexUrl(baseUrl))
if ((apiFormat === 'openai:responses' || apiFormat === 'openai:cli') && isCodex) { if (normalizedApiFormat === 'openai:responses' && isCodex) {
return '/responses' return '/responses'
} }
return defaultPath return defaultPath
@@ -2381,7 +2393,7 @@ function getCurrentUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
function isUpstreamStreamPolicyLocked(endpoint: ProviderEndpoint): boolean { function isUpstreamStreamPolicyLocked(endpoint: ProviderEndpoint): boolean {
return (props.provider?.provider_type || '').toLowerCase() === 'codex' return (props.provider?.provider_type || '').toLowerCase() === 'codex'
&& (endpoint.api_format === 'openai:responses' || endpoint.api_format === 'openai:cli') && normalizeLegacyOpenAIFormatAlias(endpoint.api_format) === 'openai:responses'
} }
// 获取上游流式按钮的样式类 // 获取上游流式按钮的样式类

View File

@@ -124,6 +124,10 @@ function normalizeUsageStreamApiFormat(value: string | null | undefined): string
switch (normalized) { switch (normalized) {
case 'openai': case 'openai':
return 'openai:chat' return 'openai:chat'
case 'openai:cli':
return 'openai:responses'
case 'openai:compact':
return 'openai:responses:compact'
case 'claude': case 'claude':
return 'claude:chat' return 'claude:chat'
case 'gemini': case 'gemini':
@@ -138,8 +142,6 @@ function usageApiFormatDefaultsToNonStream(apiFormat: string): boolean {
case 'openai:chat': case 'openai:chat':
case 'openai:responses': case 'openai:responses':
case 'openai:responses:compact': case 'openai:responses:compact':
case 'openai:cli':
case 'openai:compact':
case 'openai:image': case 'openai:image':
case 'claude:chat': case 'claude:chat':
case 'claude:cli': case 'claude:cli':