mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
migrate ai format conversion to responses adapters
This commit is contained in:
@@ -90,10 +90,10 @@ pub(super) async fn maybe_handle(
|
||||
let endpoint = match normalized_provider_type.as_str() {
|
||||
"codex" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:cli")
|
||||
&& matches!(
|
||||
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
)
|
||||
}),
|
||||
"antigravity" => endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
@@ -122,7 +122,7 @@ pub(super) async fn maybe_handle(
|
||||
|
||||
let Some(endpoint) = endpoint else {
|
||||
let detail = match normalized_provider_type.as_str() {
|
||||
"codex" => "找不到有效的 openai:cli 端点",
|
||||
"codex" => "找不到有效的 openai:responses 端点",
|
||||
"antigravity" => "找不到有效的 gemini:chat/gemini:cli 端点",
|
||||
"kiro" => "找不到有效的 Kiro 端点",
|
||||
_ => "找不到有效端点",
|
||||
|
||||
@@ -227,10 +227,10 @@ pub(super) async fn handle_admin_provider_oauth_complete_key(
|
||||
.await?;
|
||||
if let Some(endpoint) = endpoints.into_iter().find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:cli")
|
||||
&& matches!(
|
||||
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
)
|
||||
}) {
|
||||
let refreshed_key = state
|
||||
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&key_id))
|
||||
|
||||
@@ -93,8 +93,8 @@ pub(super) async fn execute_codex_quota_plan(
|
||||
body_ref: None,
|
||||
},
|
||||
stream: false,
|
||||
client_api_format: "openai:cli".to_string(),
|
||||
provider_api_format: "openai:cli".to_string(),
|
||||
client_api_format: "openai:responses".to_string(),
|
||||
provider_api_format: "openai:responses".to_string(),
|
||||
model_name: Some("codex-wham-usage".to_string()),
|
||||
proxy,
|
||||
tls_profile: state.resolve_transport_tls_profile(transport),
|
||||
|
||||
@@ -19,10 +19,10 @@ pub(crate) fn provider_oauth_runtime_endpoint_for_provider(
|
||||
.iter()
|
||||
.find(|endpoint| {
|
||||
endpoint.is_active
|
||||
&& endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("openai:cli")
|
||||
&& matches!(
|
||||
endpoint.api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
)
|
||||
})
|
||||
.cloned(),
|
||||
"antigravity" => endpoints
|
||||
|
||||
@@ -22,7 +22,6 @@ use crate::provider_transport::kiro::{
|
||||
build_kiro_provider_request_body, supports_local_kiro_request_transport_with_network,
|
||||
KiroProviderHeadersInput, KIRO_ENVELOPE_NAME,
|
||||
};
|
||||
use crate::provider_transport::url::build_openai_cli_url;
|
||||
use crate::usage::GatewaySyncReportRequest;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_admin::provider::pool as admin_provider_pool_pure;
|
||||
@@ -282,10 +281,19 @@ fn provider_query_key_supports_endpoint(
|
||||
return true;
|
||||
}
|
||||
let formats = provider_key_configured_api_formats(key);
|
||||
let endpoint_api_format = provider_query_normalize_api_format_alias(endpoint_api_format);
|
||||
formats.is_empty()
|
||||
|| formats
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(endpoint_api_format))
|
||||
.any(|value| provider_query_normalize_api_format_alias(value) == endpoint_api_format)
|
||||
}
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_query_transport_supports_standard_test_execution(
|
||||
@@ -297,7 +305,7 @@ fn provider_query_transport_supports_standard_test_execution(
|
||||
"openai:chat" => {
|
||||
crate::provider_transport::policy::supports_local_openai_chat_transport(transport)
|
||||
}
|
||||
"openai:cli" => {
|
||||
"openai:responses" | "openai:cli" => {
|
||||
crate::provider_transport::policy::supports_local_standard_transport_with_network(
|
||||
transport, api_format,
|
||||
)
|
||||
@@ -977,7 +985,7 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
}
|
||||
provider_request_body
|
||||
}
|
||||
"openai:cli" => {
|
||||
"openai:responses" | "openai:cli" => {
|
||||
let Some(mut provider_request_body) =
|
||||
crate::ai_pipeline::build_cross_format_openai_chat_request_body(
|
||||
&request_body,
|
||||
@@ -1001,14 +1009,14 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
format!("Provider request body rules rejected {provider_api_format}"),
|
||||
));
|
||||
}
|
||||
crate::ai_pipeline::apply_codex_openai_cli_special_body_edits(
|
||||
crate::ai_pipeline::apply_codex_openai_responses_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
transport.provider.provider_type.as_str(),
|
||||
provider_api_format,
|
||||
transport.endpoint.body_rules.as_ref(),
|
||||
Some(candidate.key.id.as_str()),
|
||||
);
|
||||
crate::ai_pipeline::apply_openai_compact_special_body_edits(
|
||||
crate::ai_pipeline::apply_openai_responses_compact_special_body_edits(
|
||||
&mut provider_request_body,
|
||||
provider_api_format,
|
||||
);
|
||||
@@ -1030,12 +1038,12 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
None
|
||||
};
|
||||
let oauth_auth = match provider_api_format {
|
||||
"openai:chat" | "openai:cli" | "claude:chat" | "claude:cli" | "gemini:chat"
|
||||
| "gemini:cli" => state.resolve_local_oauth_header_auth(&transport).await?,
|
||||
"openai:chat" | "openai:responses" | "openai:cli" | "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:cli" => {
|
||||
"openai:chat" | "openai:responses" | "openai:cli" => {
|
||||
crate::provider_transport::auth::resolve_local_openai_bearer_auth(&transport)
|
||||
.or(oauth_auth)
|
||||
}
|
||||
@@ -1096,7 +1104,7 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
Some("application/json"),
|
||||
)
|
||||
}
|
||||
"openai:cli" => {
|
||||
"openai:responses" | "openai:cli" => {
|
||||
crate::provider_transport::auth::build_complete_passthrough_headers_with_auth(
|
||||
&parts.headers,
|
||||
auth_header.as_deref().unwrap_or_default(),
|
||||
@@ -1150,8 +1158,11 @@ async fn provider_query_execute_standard_test_candidate(
|
||||
response_body: None,
|
||||
});
|
||||
}
|
||||
if provider_api_format == "openai:cli" {
|
||||
crate::ai_pipeline::apply_codex_openai_cli_special_headers(
|
||||
if matches!(
|
||||
provider_api_format.trim().to_ascii_lowercase().as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
) {
|
||||
crate::ai_pipeline::apply_codex_openai_responses_special_headers(
|
||||
&mut request_headers,
|
||||
&provider_request_body,
|
||||
&parts.headers,
|
||||
@@ -1254,7 +1265,10 @@ 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:cli" | "claude:cli" | "gemini:cli")
|
||||
matches!(
|
||||
api_format,
|
||||
"openai:responses" | "openai:cli" | "claude:cli" | "gemini:cli"
|
||||
)
|
||||
}
|
||||
|
||||
async fn build_admin_provider_query_kiro_failover_response(
|
||||
|
||||
@@ -328,11 +328,8 @@ fn resolve_fixed_provider_endpoint_template<'a>(
|
||||
}
|
||||
|
||||
template.endpoints.iter().find(|item| {
|
||||
item.api_format
|
||||
.eq_ignore_ascii_case(updated_endpoint.api_format.trim())
|
||||
|| item
|
||||
.api_format
|
||||
.eq_ignore_ascii_case(existing_endpoint.api_format.trim())
|
||||
api_format_matches(item.api_format, updated_endpoint.api_format.trim())
|
||||
|| api_format_matches(item.api_format, existing_endpoint.api_format.trim())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -349,6 +346,19 @@ fn endpoint_matches_fixed_provider_template(
|
||||
.api_format
|
||||
.trim()
|
||||
.eq_ignore_ascii_case(endpoint_template.api_format)
|
||||
|| api_format_matches(&endpoint.api_format, endpoint_template.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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn api_format_matches(left: &str, right: &str) -> bool {
|
||||
normalize_api_format_alias(left) == normalize_api_format_alias(right)
|
||||
}
|
||||
|
||||
fn fixed_provider_endpoint_metadata(
|
||||
|
||||
@@ -312,7 +312,16 @@ impl<'a> AdminAppState<'a> {
|
||||
)?;
|
||||
|
||||
let provider_type = provider.provider_type.trim().to_ascii_lowercase();
|
||||
if provider_type == "codex" && existing_endpoint.api_format == "openai:cli" {
|
||||
if provider_type == "codex"
|
||||
&& matches!(
|
||||
existing_endpoint
|
||||
.api_format
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
.as_str(),
|
||||
"openai:responses" | "openai:cli"
|
||||
)
|
||||
{
|
||||
let has_config_in_payload = fields.contains("config");
|
||||
let config_payload = if has_config_in_payload {
|
||||
updated
|
||||
@@ -334,7 +343,7 @@ impl<'a> AdminAppState<'a> {
|
||||
&& requested.is_some()
|
||||
&& !admin_requested_force_stream(requested.expect("checked above"))
|
||||
{
|
||||
return Err("Codex OpenAI CLI 端点固定为强制流式,不允许修改".to_string());
|
||||
return Err("Codex OpenAI Responses 端点固定为强制流式,不允许修改".to_string());
|
||||
}
|
||||
config.remove("upstreamStreamPolicy");
|
||||
config.remove("upstream_stream");
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::internal::resolve_local_proxy_execution_path;
|
||||
pub(crate) use super::public::matches_model_mapping_for_models;
|
||||
use crate::ai_pipeline_api::{
|
||||
aggregate_claude_stream_sync_response, aggregate_gemini_stream_sync_response,
|
||||
aggregate_openai_chat_stream_sync_response, aggregate_openai_cli_stream_sync_response,
|
||||
aggregate_openai_chat_stream_sync_response, aggregate_openai_responses_stream_sync_response,
|
||||
maybe_bridge_standard_sync_json_to_stream,
|
||||
};
|
||||
use crate::api::response::{
|
||||
@@ -66,7 +66,7 @@ const OPENAI_CHAT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
|
||||
"当前 OpenAI Chat Completions 请求无法在本地执行:没有匹配到可用的执行路径";
|
||||
const OPENAI_RESPONSES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
|
||||
"当前 OpenAI Responses 请求无法在本地执行:没有匹配到可用的执行路径";
|
||||
const OPENAI_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
|
||||
const OPENAI_RESPONSES_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
|
||||
"当前 OpenAI Responses Compact 请求无法在本地执行:没有匹配到可用的执行路径";
|
||||
const OPENAI_VIDEO_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL: &str =
|
||||
"当前 OpenAI Video 请求无法在本地执行:没有匹配到可用的执行路径";
|
||||
@@ -463,10 +463,12 @@ fn aggregate_sync_sse_response_for_client(
|
||||
aggregate_openai_chat_stream_sync_response(body)
|
||||
}
|
||||
Some(value)
|
||||
if value.eq_ignore_ascii_case("openai:cli")
|
||||
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") =>
|
||||
{
|
||||
aggregate_openai_cli_stream_sync_response(body)
|
||||
aggregate_openai_responses_stream_sync_response(body)
|
||||
}
|
||||
Some(value)
|
||||
if value.eq_ignore_ascii_case("claude:chat")
|
||||
@@ -484,7 +486,7 @@ fn aggregate_sync_sse_response_for_client(
|
||||
aggregate_openai_chat_stream_sync_response(body)
|
||||
}
|
||||
_ if public_path == "/v1/responses" || public_path == "/v1/responses/compact" => {
|
||||
aggregate_openai_cli_stream_sync_response(body)
|
||||
aggregate_openai_responses_stream_sync_response(body)
|
||||
}
|
||||
_ if public_path == "/v1/messages" => aggregate_claude_stream_sync_response(body),
|
||||
_ if decision.route_family.as_deref() == Some("gemini")
|
||||
@@ -531,15 +533,25 @@ fn resolve_affinity_forward_client_api_format(
|
||||
.filter(|value| !value.is_empty());
|
||||
match api_format {
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:chat") => Some("openai:chat"),
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:cli") => Some("openai:cli"),
|
||||
Some(value) if value.eq_ignore_ascii_case("openai:compact") => Some("openai:compact"),
|
||||
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("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:cli") => Some("claude:cli"),
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:chat") => Some("gemini:chat"),
|
||||
Some(value) if value.eq_ignore_ascii_case("gemini:cli") => Some("gemini:cli"),
|
||||
_ if public_path == "/v1/chat/completions" => Some("openai:chat"),
|
||||
_ if public_path == "/v1/responses" => Some("openai:cli"),
|
||||
_ if public_path == "/v1/responses/compact" => Some("openai:compact"),
|
||||
_ if public_path == "/v1/responses" => Some("openai:responses"),
|
||||
_ if public_path == "/v1/responses/compact" => Some("openai:responses:compact"),
|
||||
_ if public_path == "/v1/messages" => Some("claude:chat"),
|
||||
_ if decision.route_family.as_deref() == Some("gemini")
|
||||
&& (public_path.contains(":generateContent")
|
||||
@@ -1702,7 +1714,9 @@ fn local_execution_runtime_miss_route_detail(
|
||||
match public_path {
|
||||
"/v1/chat/completions" => Some(OPENAI_CHAT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
|
||||
"/v1/responses" => Some(OPENAI_RESPONSES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
|
||||
"/v1/responses/compact" => Some(OPENAI_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
|
||||
"/v1/responses/compact" => {
|
||||
Some(OPENAI_RESPONSES_COMPACT_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL)
|
||||
}
|
||||
"/v1/messages" => Some(CLAUDE_MESSAGES_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL),
|
||||
path if path.starts_with("/v1/videos") => {
|
||||
Some(OPENAI_VIDEO_LOCAL_EXECUTION_RUNTIME_MISS_DETAIL)
|
||||
@@ -1784,8 +1798,8 @@ mod tests {
|
||||
"/v1/responses",
|
||||
Some("ai_public".to_string()),
|
||||
Some("openai".to_string()),
|
||||
Some("cli".to_string()),
|
||||
Some("openai:cli".to_string()),
|
||||
Some("responses".to_string()),
|
||||
Some("openai:responses".to_string()),
|
||||
);
|
||||
let diagnostic = LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: "all_candidates_skipped".to_string(),
|
||||
@@ -1815,8 +1829,8 @@ mod tests {
|
||||
"/v1/responses",
|
||||
Some("ai_public".to_string()),
|
||||
Some("openai".to_string()),
|
||||
Some("cli".to_string()),
|
||||
Some("openai:cli".to_string()),
|
||||
Some("responses".to_string()),
|
||||
Some("openai:responses".to_string()),
|
||||
);
|
||||
let diagnostic = LocalExecutionRuntimeMissDiagnostic {
|
||||
reason: "all_candidates_skipped".to_string(),
|
||||
|
||||
@@ -6,20 +6,27 @@ use aether_scheduler_core::matches_model_mapping;
|
||||
use super::GatewayPublicRequestContext;
|
||||
|
||||
pub(crate) fn models_api_format(request_context: &GatewayPublicRequestContext) -> Option<&str> {
|
||||
request_context
|
||||
let signature = request_context
|
||||
.control_decision
|
||||
.as_ref()
|
||||
.and_then(|decision| decision.auth_endpoint_signature.as_deref())
|
||||
.filter(|signature| {
|
||||
matches!(
|
||||
*signature,
|
||||
"openai:chat" | "openai:image" | "claude:chat" | "gemini:chat"
|
||||
)
|
||||
})
|
||||
.map(str::trim)
|
||||
.filter(|signature| !signature.is_empty())?;
|
||||
match signature.to_ascii_lowercase().as_str() {
|
||||
"openai:chat" => Some("openai:chat"),
|
||||
"openai:responses" | "openai:cli" => Some("openai:responses"),
|
||||
"openai:responses:compact" | "openai:compact" => Some("openai:responses:compact"),
|
||||
"openai:image" => Some("openai:image"),
|
||||
"claude:chat" => Some("claude:chat"),
|
||||
"gemini:chat" => Some("gemini:chat"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const MODELS_CROSS_FORMAT_QUERY_API_FORMATS: &[&str] = &[
|
||||
"openai:chat",
|
||||
"openai:responses",
|
||||
"openai:responses:compact",
|
||||
"openai:cli",
|
||||
"openai:compact",
|
||||
"openai:image",
|
||||
@@ -31,7 +38,13 @@ 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() {
|
||||
"openai:chat" | "claude:chat" | "gemini:chat" => MODELS_CROSS_FORMAT_QUERY_API_FORMATS,
|
||||
"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"],
|
||||
_ => &[],
|
||||
}
|
||||
|
||||
@@ -213,6 +213,8 @@ fn users_me_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAud
|
||||
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")
|
||||
@@ -1424,10 +1426,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_usage_stream_defaults_to_non_stream_for_openai_cli_request_body_without_flag() {
|
||||
fn user_usage_stream_defaults_to_non_stream_for_openai_responses_request_body_without_flag() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
is_stream: true,
|
||||
api_format: Some("openai:cli".to_string()),
|
||||
api_format: Some("openai:responses".to_string()),
|
||||
request_body: Some(json!({
|
||||
"model": "gpt-5.4",
|
||||
"input": [{"role": "user", "content": "hi"}],
|
||||
|
||||
Reference in New Issue
Block a user