Normalize canonical API formats

This commit is contained in:
fawney19
2026-04-29 09:25:19 +08:00
parent 02ad67fe33
commit 07a319259b
171 changed files with 2460 additions and 1962 deletions

View File

@@ -839,13 +839,12 @@ fn admin_usage_api_format_defaults_to_non_stream(item: &StoredRequestUsageAudit)
return false;
};
matches!(
aether_ai_formats::normalize_legacy_openai_format_alias(value).as_str(),
aether_ai_formats::normalize_api_format_alias(value).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "openai:image"
| "claude:chat"
| "claude:cli"
| "claude:messages"
)
}

View File

@@ -533,13 +533,13 @@ const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
value: "openai:responses",
label: "OpenAI Responses",
default_path: "/v1/responses",
aliases: &["openai_cli", "openai:cli", "responses"],
aliases: &["responses"],
},
AdminApiFormatDefinition {
value: "openai:responses:compact",
label: "OpenAI Responses Compact",
default_path: "/v1/responses/compact",
aliases: &["openai_compact", "openai:compact", "responses_compact"],
aliases: &["responses_compact"],
},
AdminApiFormatDefinition {
value: "openai:image",
@@ -554,29 +554,17 @@ const ADMIN_API_FORMAT_DEFINITIONS: &[AdminApiFormatDefinition] = &[
aliases: &["openai_video", "sora"],
},
AdminApiFormatDefinition {
value: "claude:chat",
label: "Claude Chat",
value: "claude:messages",
label: "Claude Messages",
default_path: "/v1/messages",
aliases: &["claude", "anthropic", "claude_compatible"],
},
AdminApiFormatDefinition {
value: "claude:cli",
label: "Claude CLI",
default_path: "/v1/messages",
aliases: &["claude_cli", "claude-cli"],
},
AdminApiFormatDefinition {
value: "gemini:chat",
label: "Gemini Chat",
value: "gemini:generate_content",
label: "Gemini Generate Content",
default_path: "/v1beta/models/{model}:{action}",
aliases: &["gemini", "google", "vertex"],
},
AdminApiFormatDefinition {
value: "gemini:cli",
label: "Gemini CLI",
default_path: "/v1beta/models/{model}:{action}",
aliases: &["gemini_cli", "gemini-cli"],
},
AdminApiFormatDefinition {
value: "gemini:video",
label: "Gemini Video",
@@ -1994,7 +1982,7 @@ mod tests {
#[test]
fn resolve_admin_system_export_key_api_formats_uses_endpoint_fallback() {
let provider_formats = vec!["openai:chat".to_string(), "claude:chat".to_string()];
let provider_formats = vec!["openai:chat".to_string(), "claude:messages".to_string()];
let resolved =
resolve_admin_system_export_key_api_formats(None, &provider_formats, |value| {
Some(value.to_string())

View File

@@ -82,55 +82,40 @@ impl FromStr for FormatId {
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"openai" | "openai:chat" | "/v1/chat/completions" => Ok(Self::OpenAiChat),
"openai:responses" | "openai:cli" | "/v1/responses" => Ok(Self::OpenAiResponses),
"openai:responses:compact" | "openai:compact" | "/v1/responses/compact" => {
"openai:responses" | "/v1/responses" => Ok(Self::OpenAiResponses),
"openai:responses:compact" | "/v1/responses/compact" => {
Ok(Self::OpenAiResponsesCompact)
}
"claude:messages" | "claude:chat" | "claude:cli" | "/v1/messages" => {
Ok(Self::ClaudeMessages)
}
"gemini:generate_content" | "gemini:chat" | "gemini:cli" => {
Ok(Self::GeminiGenerateContent)
}
"claude:messages" | "/v1/messages" => Ok(Self::ClaudeMessages),
"gemini:generate_content" => Ok(Self::GeminiGenerateContent),
_ => Err(()),
}
}
}
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 normalize_api_format_alias(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
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 api_format_alias_matches(left: &str, right: &str) -> bool {
normalize_api_format_alias(left) == normalize_api_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 api_format_storage_aliases(value: &str) -> Vec<String> {
vec![normalize_api_format_alias(value)]
}
pub fn is_openai_responses_format(value: &str) -> bool {
normalize_legacy_openai_format_alias(value) == "openai:responses"
normalize_api_format_alias(value) == "openai:responses"
}
pub fn is_openai_responses_compact_format(value: &str) -> bool {
normalize_legacy_openai_format_alias(value) == "openai:responses:compact"
normalize_api_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(),
normalize_api_format_alias(value).as_str(),
"openai:responses" | "openai:responses:compact"
)
}
@@ -138,66 +123,66 @@ pub fn is_openai_responses_family_format(value: &str) -> bool {
#[cfg(test)]
mod tests {
use super::{
legacy_openai_format_alias_matches, normalize_legacy_openai_format_alias,
openai_format_storage_aliases, FormatId,
api_format_alias_matches, api_format_storage_aliases, normalize_api_format_alias, FormatId,
};
#[test]
fn normalizes_legacy_aliases() {
assert_eq!(
FormatId::parse("openai:cli"),
Some(FormatId::OpenAiResponses)
);
assert_eq!(
FormatId::parse("openai:compact"),
Some(FormatId::OpenAiResponsesCompact)
);
assert_eq!(
FormatId::parse("claude:cli"),
Some(FormatId::ClaudeMessages)
);
assert_eq!(
FormatId::parse("gemini:chat"),
Some(FormatId::GeminiGenerateContent)
);
fn retired_api_formats_do_not_parse() {
assert_eq!(FormatId::parse("openai:cli"), None);
assert_eq!(FormatId::parse("openai:compact"), None);
assert_eq!(FormatId::parse("claude:chat"), None);
assert_eq!(FormatId::parse("claude:cli"), None);
assert_eq!(FormatId::parse("gemini:chat"), None);
assert_eq!(FormatId::parse("gemini:cli"), None);
}
#[test]
fn normalizes_openai_legacy_aliases_without_rewriting_other_families() {
fn normalizes_api_format_aliases() {
assert_eq!(
normalize_legacy_openai_format_alias(" openai:cli "),
normalize_api_format_alias(" OPENAI:RESPONSES "),
"openai:responses"
);
assert_eq!(
normalize_legacy_openai_format_alias("OPENAI:COMPACT"),
normalize_api_format_alias("OPENAI:RESPONSES:COMPACT"),
"openai:responses:compact"
);
assert_eq!(
normalize_legacy_openai_format_alias("claude:cli"),
"claude:cli"
normalize_api_format_alias("CLAUDE:MESSAGES"),
"claude:messages"
);
assert!(legacy_openai_format_alias_matches(
"openai:cli",
"openai:responses"
assert_eq!(
normalize_api_format_alias("GEMINI:GENERATE_CONTENT"),
"gemini:generate_content"
);
assert_eq!(normalize_api_format_alias("openai:image"), "openai:image");
assert_eq!(normalize_api_format_alias("openai:video"), "openai:video");
assert_eq!(normalize_api_format_alias("gemini:video"), "gemini:video");
assert_eq!(normalize_api_format_alias("gemini:files"), "gemini:files");
assert!(!api_format_alias_matches("claude:cli", "claude:messages"));
assert!(!api_format_alias_matches(
"gemini:chat",
"gemini:generate_content"
));
assert!(!api_format_alias_matches("openai:cli", "openai:responses"));
}
#[test]
fn storage_aliases_include_legacy_openai_rows_only() {
fn storage_aliases_only_include_normalized_value() {
assert_eq!(
openai_format_storage_aliases("openai:responses"),
vec!["openai:responses".to_string(), "openai:cli".to_string()]
api_format_storage_aliases("openai:responses"),
vec!["openai:responses".to_string()]
);
assert_eq!(
openai_format_storage_aliases("openai:compact"),
vec![
"openai:responses:compact".to_string(),
"openai:compact".to_string()
]
api_format_storage_aliases("openai:responses:compact"),
vec!["openai:responses:compact".to_string()]
);
assert_eq!(
openai_format_storage_aliases("claude:cli"),
vec!["claude:cli".to_string()]
api_format_storage_aliases("claude:messages"),
vec!["claude:messages".to_string()]
);
assert_eq!(
api_format_storage_aliases("gemini:generate_content"),
vec!["gemini:generate_content".to_string()]
);
}
}

View File

@@ -26,10 +26,9 @@ pub use canonical::{
};
pub use context::{FormatContext, FormatError};
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,
api_format_alias_matches, api_format_storage_aliases, is_openai_responses_compact_format,
is_openai_responses_family_format, is_openai_responses_format, normalize_api_format_alias,
FormatFamily, FormatId, FormatProfile,
};
pub use matrix::{
request_candidate_api_format_preference, request_candidate_api_formats,

View File

@@ -1,6 +1,6 @@
use crate::{
formats::{is_openai_responses_compact_format, normalize_legacy_openai_format_alias},
legacy_openai_format_alias_matches,
api_format_alias_matches,
formats::{is_openai_responses_compact_format, normalize_api_format_alias},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -28,10 +28,8 @@ pub enum SyncCliResponseConversionKind {
const NON_COMPACT_STANDARD_CANDIDATE_API_FORMATS: &[&str] = &[
"openai:chat",
"openai:responses",
"claude:chat",
"claude:cli",
"gemini:chat",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
];
const STANDARD_API_FAMILY_ORDER: &[&str] = &["openai", "claude", "gemini"];
@@ -39,8 +37,8 @@ pub fn request_candidate_api_format_preference(
client_api_format: &str,
provider_api_format: &str,
) -> Option<(u8, u8)> {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == "openai:responses:compact" {
return (provider_api_format == "openai:responses:compact").then_some((0, 0));
@@ -70,7 +68,7 @@ pub fn request_candidate_api_formats(
client_api_format: &str,
_require_streaming: bool,
) -> Vec<&'static str> {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
if client_api_format == "openai:responses:compact" {
return vec!["openai:responses:compact"];
}
@@ -90,8 +88,8 @@ pub fn request_conversion_kind(
client_api_format: &str,
provider_api_format: &str,
) -> Option<RequestConversionKind> {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == provider_api_format {
return None;
}
@@ -109,8 +107,8 @@ pub fn request_conversion_kind(
match provider_api_format.as_str() {
"openai:chat" => Some(RequestConversionKind::ToOpenAIChat),
"openai:responses" => Some(RequestConversionKind::ToOpenAiResponses),
"claude:chat" | "claude:cli" => Some(RequestConversionKind::ToClaudeStandard),
"gemini:chat" | "gemini:cli" => Some(RequestConversionKind::ToGeminiStandard),
"claude:messages" => Some(RequestConversionKind::ToClaudeStandard),
"gemini:generate_content" => Some(RequestConversionKind::ToGeminiStandard),
_ => None,
}
}
@@ -119,8 +117,8 @@ pub fn sync_chat_response_conversion_kind(
provider_api_format: &str,
client_api_format: &str,
) -> Option<SyncChatResponseConversionKind> {
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
if provider_api_format == client_api_format {
return None;
}
@@ -130,8 +128,8 @@ pub fn sync_chat_response_conversion_kind(
request_conversion_kind(client_api_format.as_str(), provider_api_format.as_str())?;
match client_api_format.as_str() {
"openai:chat" => Some(SyncChatResponseConversionKind::ToOpenAIChat),
"claude:chat" => Some(SyncChatResponseConversionKind::ToClaudeChat),
"gemini:chat" => Some(SyncChatResponseConversionKind::ToGeminiChat),
"claude:messages" => Some(SyncChatResponseConversionKind::ToClaudeChat),
"gemini:generate_content" => Some(SyncChatResponseConversionKind::ToGeminiChat),
_ => None,
}
}
@@ -140,8 +138,8 @@ pub fn sync_cli_response_conversion_kind(
provider_api_format: &str,
client_api_format: &str,
) -> Option<SyncCliResponseConversionKind> {
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
if provider_api_format == client_api_format {
return None;
}
@@ -155,8 +153,8 @@ pub fn sync_cli_response_conversion_kind(
"openai:responses" | "openai:responses:compact" => {
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
}
"claude:cli" => Some(SyncCliResponseConversionKind::ToClaudeCli),
"gemini:cli" => Some(SyncCliResponseConversionKind::ToGeminiCli),
"claude:messages" => Some(SyncCliResponseConversionKind::ToClaudeCli),
"gemini:generate_content" => Some(SyncCliResponseConversionKind::ToGeminiCli),
_ => None,
}
}
@@ -165,8 +163,8 @@ pub fn request_conversion_requires_enable_flag(
client_api_format: &str,
provider_api_format: &str,
) -> bool {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
match (
api_data_format_id(client_api_format.as_str()),
api_data_format_id(provider_api_format.as_str()),
@@ -180,35 +178,31 @@ pub fn request_conversion_requires_enable_flag(
pub fn is_standard_api_format(api_format: &str) -> bool {
matches!(
normalize_legacy_openai_format_alias(api_format).as_str(),
normalize_api_format_alias(api_format).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
| "gemini:cli"
| "claude:messages"
| "gemini:generate_content"
)
}
pub fn parse_non_compact_standard_api_format(
api_format: &str,
) -> Option<(&'static str, &'static str)> {
match normalize_legacy_openai_format_alias(api_format).as_str() {
match normalize_api_format_alias(api_format).as_str() {
"openai:chat" => Some(("openai", "chat")),
"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")),
"openai:responses" => Some(("openai", "responses")),
"claude:messages" => Some(("claude", "messages")),
"gemini:generate_content" => Some(("gemini", "generate_content")),
_ => None,
}
}
pub fn api_data_format_id(api_format: &str) -> Option<&'static str> {
match normalize_legacy_openai_format_alias(api_format).as_str() {
"claude:chat" | "claude:cli" => Some("claude"),
"gemini:chat" | "gemini:cli" => Some("gemini"),
match normalize_api_format_alias(api_format).as_str() {
"claude:messages" => Some("claude"),
"gemini:generate_content" => Some("gemini"),
"openai:chat" => Some("openai_chat"),
"openai:responses" | "openai:responses:compact" => Some("openai_responses"),
_ => None,
@@ -216,7 +210,7 @@ pub fn api_data_format_id(api_format: &str) -> Option<&'static str> {
}
pub fn normalized_same_standard_api_format(left: &str, right: &str) -> bool {
legacy_openai_format_alias_matches(left, right)
api_format_alias_matches(left, right)
}
fn standard_api_family_priority(family: &str) -> u8 {
@@ -239,8 +233,8 @@ mod tests {
match provider_api_format {
"openai:chat" => RequestConversionKind::ToOpenAIChat,
"openai:responses" => RequestConversionKind::ToOpenAiResponses,
"claude:chat" | "claude:cli" => RequestConversionKind::ToClaudeStandard,
"gemini:chat" | "gemini:cli" => RequestConversionKind::ToGeminiStandard,
"claude:messages" => RequestConversionKind::ToClaudeStandard,
"gemini:generate_content" => RequestConversionKind::ToGeminiStandard,
other => panic!("unexpected provider format {other}"),
}
}
@@ -252,7 +246,7 @@ mod tests {
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:cli"),
request_conversion_kind("openai:chat", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
@@ -260,11 +254,11 @@ mod tests {
Some(RequestConversionKind::ToOpenAIChat)
);
assert_eq!(
request_conversion_kind("openai:responses:compact", "gemini:cli"),
request_conversion_kind("openai:responses:compact", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:cli", "openai:responses:compact"),
request_conversion_kind("gemini:generate_content", "openai:responses:compact"),
None
);
assert_eq!(
@@ -280,22 +274,20 @@ mod tests {
None
);
assert_eq!(
request_conversion_kind("gemini:chat", "claude:chat"),
request_conversion_kind("gemini:generate_content", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(request_conversion_kind("claude:chat", "claude:cli"), None);
assert_eq!(
request_conversion_kind("claude:chat", "claude:cli"),
Some(RequestConversionKind::ToClaudeStandard)
request_conversion_kind("claude:messages", "claude:messages"),
None
);
assert_eq!(request_conversion_kind("claude:chat", "claude:chat"), None);
let formats = [
"openai:chat",
"openai:responses",
"claude:chat",
"claude:cli",
"gemini:chat",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
];
for client_api_format in formats {
for provider_api_format in formats {
@@ -316,35 +308,35 @@ mod tests {
#[test]
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
sync_chat_response_conversion_kind("openai:chat", "claude:chat"),
sync_chat_response_conversion_kind("openai:chat", "claude:messages"),
Some(SyncChatResponseConversionKind::ToClaudeChat)
);
assert_eq!(
sync_chat_response_conversion_kind("claude:chat", "gemini:chat"),
sync_chat_response_conversion_kind("claude:messages", "gemini:generate_content"),
Some(SyncChatResponseConversionKind::ToGeminiChat)
);
assert_eq!(
sync_chat_response_conversion_kind("gemini:chat", "openai:chat"),
sync_chat_response_conversion_kind("gemini:generate_content", "openai:chat"),
Some(SyncChatResponseConversionKind::ToOpenAIChat)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "gemini:cli"),
sync_cli_response_conversion_kind("openai:responses", "gemini:generate_content"),
Some(SyncCliResponseConversionKind::ToGeminiCli)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:chat", "openai:responses"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:cli", "openai:responses:compact"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses:compact"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses:compact", "claude:cli"),
sync_cli_response_conversion_kind("openai:responses:compact", "claude:messages"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("gemini:cli", "claude:cli"),
sync_cli_response_conversion_kind("gemini:generate_content", "claude:messages"),
Some(SyncCliResponseConversionKind::ToClaudeCli)
);
assert_eq!(
@@ -363,60 +355,53 @@ mod tests {
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"claude:chat",
"gemini:chat",
"openai:responses",
"claude:cli",
"gemini:cli"
"claude:messages",
"gemini:generate_content"
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat"
"claude:messages",
"gemini:generate_content"
]
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
request_candidate_api_formats("openai:responses", false)
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
vec![
"claude:cli",
"openai:responses",
"gemini:cli",
"claude:chat",
"openai:chat",
"gemini:chat"
]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
vec!["openai:responses:compact"]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:responses"),
Some((1, 0))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
Some((2, 1))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
Some((3, 0))
None
);
}
#[test]
fn request_conversion_enable_flag_only_applies_to_real_data_format_conversions() {
assert!(!request_conversion_requires_enable_flag(
"claude:messages",
"claude:messages"
));
assert!(request_conversion_requires_enable_flag(
"claude:chat",
"claude:cli"
));
@@ -425,8 +410,8 @@ mod tests {
"openai:responses"
));
assert!(request_conversion_requires_enable_flag(
"claude:chat",
"gemini:chat"
"claude:messages",
"gemini:generate_content"
));
assert!(request_conversion_requires_enable_flag(
"openai:compact",

View File

@@ -147,11 +147,8 @@ mod tests {
use crate::formats::FormatId;
#[test]
fn cli_alias_routes_to_openai_responses() {
assert_eq!(
FormatId::parse("openai:cli"),
Some(FormatId::OpenAiResponses)
);
fn openai_cli_alias_is_not_a_primary_format() {
assert_eq!(FormatId::parse("openai:cli"), None);
}
#[test]

View File

@@ -25,7 +25,6 @@ pub fn provider_private_response_allows_sync_finalize(report_context: &Value) ->
.and_then(Value::as_str)
.unwrap_or_default();
provider_adaptation_allows_sync_finalize_envelope(envelope_name, provider_api_format)
|| matches!(envelope_name, "claude:cli")
}
pub fn normalize_provider_private_report_context(report_context: Option<&Value>) -> Option<Value> {
@@ -63,7 +62,7 @@ pub fn normalize_provider_private_response_value(
return Some(data);
}
let mut unwrapped = match report_context.get("envelope_name").and_then(Value::as_str) {
Some("claude:cli") | Some(KIRO_ENVELOPE_NAME) => data,
Some(KIRO_ENVELOPE_NAME) => data,
Some(GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME) => {
if let Some(response) = data
.get("response")
@@ -345,7 +344,7 @@ mod tests {
let report_context = json!({
"has_envelope": true,
"envelope_name": "antigravity:v1internal",
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
});
let normalized = normalize_provider_private_report_context(Some(&report_context))
.expect("context should normalize");
@@ -357,7 +356,7 @@ mod tests {
fn unwraps_antigravity_sync_response_and_injects_ids() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"mapped_model": "claude-sonnet-4-5",
});
@@ -390,8 +389,8 @@ mod tests {
fn unwraps_antigravity_stream_line_and_injects_ids() {
let report_context = json!({
"has_envelope": true,
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"mapped_model": "claude-sonnet-4-5",
});

View File

@@ -30,7 +30,7 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
surface: ProviderAdaptationSurface::AntigravityGeminiChat,
provider_type: Some(ANTIGRAVITY_PROVIDER_TYPE),
envelope_name: ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:chat",
anchor_api_format: "gemini:generate_content",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
@@ -41,7 +41,7 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
surface: ProviderAdaptationSurface::AntigravityGeminiCli,
provider_type: Some(ANTIGRAVITY_PROVIDER_TYPE),
envelope_name: ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:cli",
anchor_api_format: "gemini:generate_content",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
@@ -52,7 +52,7 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
surface: ProviderAdaptationSurface::GeminiCliV1Internal,
provider_type: None,
envelope_name: GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
anchor_api_format: "gemini:cli",
anchor_api_format: "gemini:generate_content",
supports_request_bridge: false,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
@@ -63,7 +63,7 @@ const PROVIDER_ADAPTATION_SURFACES: &[ProviderAdaptationDescriptor] = &[
surface: ProviderAdaptationSurface::KiroClaudeCli,
provider_type: Some(KIRO_PROVIDER_TYPE),
envelope_name: KIRO_ENVELOPE_NAME,
anchor_api_format: "claude:cli",
anchor_api_format: "claude:messages",
supports_request_bridge: true,
supports_sync_finalize_bridge: true,
supports_stream_bridge: true,
@@ -149,20 +149,20 @@ mod tests {
assert_eq!(
provider_adaptation_anchor_api_format(
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
"gemini:cli"
"gemini:generate_content"
),
Some("gemini:cli")
Some("gemini:generate_content")
);
assert_eq!(
provider_adaptation_anchor_api_format(
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
"gemini:cli"
"gemini:generate_content"
),
Some("gemini:cli")
Some("gemini:generate_content")
);
assert_eq!(
provider_adaptation_anchor_api_format(KIRO_ENVELOPE_NAME, "claude:cli"),
Some("claude:cli")
provider_adaptation_anchor_api_format(KIRO_ENVELOPE_NAME, "claude:messages"),
Some("claude:messages")
);
}
@@ -170,19 +170,19 @@ mod tests {
fn exposes_private_surface_capabilities() {
assert!(provider_adaptation_allows_sync_finalize_envelope(
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME,
"gemini:chat"
"gemini:generate_content"
));
assert!(provider_adaptation_should_unwrap_stream_envelope(
GEMINI_CLI_V1INTERNAL_ENVELOPE_NAME,
"gemini:cli"
"gemini:generate_content"
));
assert!(provider_adaptation_requires_eventstream_accept(
Some(KIRO_ENVELOPE_NAME),
"claude:cli"
"claude:messages"
));
assert!(!provider_adaptation_requires_eventstream_accept(
Some(ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME),
"gemini:cli"
"gemini:generate_content"
));
}
}

View File

@@ -190,7 +190,6 @@ pub use crate::planner::standard::{
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,
api_format_alias_matches, api_format_storage_aliases, is_openai_responses_compact_format,
is_openai_responses_family_format, is_openai_responses_format, normalize_api_format_alias,
};

View File

@@ -67,15 +67,15 @@ pub fn implicit_sync_finalize_report_kind(plan_kind: &str) -> Option<&'static st
pub fn core_error_default_client_api_format(report_kind: &str) -> Option<&'static str> {
match report_kind {
OPENAI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("openai:chat"),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:chat"),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:chat"),
CLAUDE_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"),
GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"),
OPENAI_RESPONSES_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"),
OPENAI_RESPONSES_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"),
LEGACY_OPENAI_CLI_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses"),
LEGACY_OPENAI_COMPACT_SYNC_FINALIZE_REPORT_KIND => Some("openai:responses:compact"),
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND => Some("openai:image"),
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND => Some("claude:cli"),
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND => Some("gemini:cli"),
CLAUDE_CLI_SYNC_FINALIZE_REPORT_KIND => Some("claude:messages"),
GEMINI_CLI_SYNC_FINALIZE_REPORT_KIND => Some("gemini:generate_content"),
_ => None,
}
}

View File

@@ -37,7 +37,7 @@ pub fn build_core_error_body_for_client_format(
let mut error_object = Map::new();
error_object.insert("message".to_string(), Value::String(message.to_string()));
match aether_ai_formats::normalize_legacy_openai_format_alias(client_api_format).as_str() {
match aether_ai_formats::normalize_api_format_alias(client_api_format).as_str() {
"openai:chat" | "openai:responses" | "openai:responses:compact" => {
error_object.insert(
"type".to_string(),
@@ -51,7 +51,7 @@ pub fn build_core_error_body_for_client_format(
Value::Object(error_object),
)])))
}
"claude:chat" | "claude:cli" => {
"claude:messages" => {
error_object.insert(
"type".to_string(),
Value::String(map_local_sync_error_kind_to_claude_type(kind).to_string()),
@@ -64,7 +64,7 @@ pub fn build_core_error_body_for_client_format(
("error".to_string(), Value::Object(error_object)),
])))
}
"gemini:chat" | "gemini:cli" => Some(Value::Object(Map::from_iter([(
"gemini:generate_content" => Some(Value::Object(Map::from_iter([(
"error".to_string(),
Value::Object(Map::from_iter([
(

View File

@@ -1,6 +1,6 @@
#![allow(dead_code)]
use aether_ai_formats::normalize_legacy_openai_format_alias;
use aether_ai_formats::normalize_api_format_alias;
use aether_provider_transport::auth::{
resolve_local_gemini_auth, resolve_local_openai_bearer_auth, resolve_local_standard_auth,
};
@@ -80,8 +80,8 @@ pub fn request_conversion_enabled_for_transport(
client_api_format: &str,
provider_api_format: &str,
) -> bool {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == provider_api_format {
return true;
}
@@ -103,8 +103,8 @@ pub fn request_pair_allowed_for_transport(
client_api_format: &str,
provider_api_format: &str,
) -> bool {
let client_api_format = normalize_legacy_openai_format_alias(client_api_format);
let provider_api_format = normalize_legacy_openai_format_alias(provider_api_format);
let client_api_format = normalize_api_format_alias(client_api_format);
let provider_api_format = normalize_api_format_alias(provider_api_format);
if client_api_format == provider_api_format {
return true;
}
@@ -116,7 +116,7 @@ pub fn request_pair_allowed_for_transport(
.provider_type
.trim()
.eq_ignore_ascii_case("kiro")
&& provider_api_format.eq_ignore_ascii_case("claude:cli")
&& aether_ai_formats::api_format_alias_matches(&provider_api_format, "claude:messages")
{
return request_conversion_enabled_for_transport(
transport,
@@ -148,16 +148,15 @@ pub fn request_conversion_transport_unsupported_reason(
.provider_type
.trim()
.eq_ignore_ascii_case("kiro")
&& transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case("claude:cli")
&& aether_ai_formats::api_format_alias_matches(
&transport.endpoint.api_format,
"claude:messages",
)
{
return local_kiro_request_transport_unsupported_reason_with_network(transport);
}
match normalize_legacy_openai_format_alias(&transport.endpoint.api_format).as_str() {
match normalize_api_format_alias(&transport.endpoint.api_format).as_str() {
"openai:chat" => local_openai_chat_transport_unsupported_reason(transport),
"openai:responses" | "openai:responses:compact" => {
local_standard_transport_unsupported_reason_with_network(
@@ -165,21 +164,16 @@ pub fn request_conversion_transport_unsupported_reason(
transport.endpoint.api_format.trim(),
)
}
"claude:chat" => {
local_standard_transport_unsupported_reason_with_network(transport, "claude:chat")
"claude:messages" => {
local_standard_transport_unsupported_reason_with_network(transport, "claude:messages")
}
"claude:cli" => {
local_standard_transport_unsupported_reason_with_network(transport, "claude:cli")
}
"gemini:chat" | "gemini:cli" if is_vertex_api_key_transport_context(transport) => {
"gemini:generate_content" if is_vertex_api_key_transport_context(transport) => {
local_vertex_api_key_gemini_transport_unsupported_reason_with_network(transport)
}
"gemini:chat" => {
local_gemini_transport_unsupported_reason_with_network(transport, "gemini:chat")
}
"gemini:cli" => {
local_gemini_transport_unsupported_reason_with_network(transport, "gemini:cli")
}
"gemini:generate_content" => local_gemini_transport_unsupported_reason_with_network(
transport,
"gemini:generate_content",
),
_ => Some("transport_api_format_unsupported"),
}
}
@@ -188,11 +182,11 @@ pub fn request_conversion_direct_auth(
transport: &GatewayProviderTransportSnapshot,
_kind: RequestConversionKind,
) -> Option<(String, String)> {
match normalize_legacy_openai_format_alias(&transport.endpoint.api_format).as_str() {
match normalize_api_format_alias(&transport.endpoint.api_format).as_str() {
"openai:chat" | "openai:responses" | "openai:responses:compact" => {
resolve_local_openai_bearer_auth(transport)
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
if is_vertex_api_key_transport_context(transport) {
resolve_local_vertex_api_key_query_auth(transport)
.map(|auth| (VERTEX_API_KEY_QUERY_PARAM.to_string(), auth.value))
@@ -200,7 +194,7 @@ pub fn request_conversion_direct_auth(
resolve_local_gemini_auth(transport)
}
}
"claude:chat" | "claude:cli" => resolve_local_standard_auth(transport),
"claude:messages" => resolve_local_standard_auth(transport),
_ => None,
}
}
@@ -243,8 +237,9 @@ fn json_format_list_contains(value: &serde_json::Value, api_format: &str) -> boo
return false;
};
items.iter().any(|item| {
item.as_str()
.is_some_and(|candidate| candidate.trim().eq_ignore_ascii_case(api_format))
item.as_str().is_some_and(|candidate| {
aether_ai_formats::api_format_alias_matches(candidate, api_format)
})
})
}
@@ -266,18 +261,16 @@ mod tests {
const STANDARD_SURFACES: &[&str] = &[
"openai:chat",
"openai:responses",
"claude:chat",
"claude:cli",
"gemini:chat",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
];
fn expected_request_conversion_kind(provider_api_format: &str) -> RequestConversionKind {
match provider_api_format {
"openai:chat" => RequestConversionKind::ToOpenAIChat,
"openai:responses" => RequestConversionKind::ToOpenAiResponses,
"claude:chat" | "claude:cli" => RequestConversionKind::ToClaudeStandard,
"gemini:chat" | "gemini:cli" => RequestConversionKind::ToGeminiStandard,
"claude:messages" => RequestConversionKind::ToClaudeStandard,
"gemini:generate_content" => RequestConversionKind::ToGeminiStandard,
other => panic!("unexpected provider api format: {other}"),
}
}
@@ -289,7 +282,7 @@ mod tests {
Some(RequestConversionKind::ToOpenAiResponses)
);
assert_eq!(
request_conversion_kind("openai:chat", "claude:cli"),
request_conversion_kind("openai:chat", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(
@@ -297,11 +290,11 @@ mod tests {
Some(RequestConversionKind::ToOpenAIChat)
);
assert_eq!(
request_conversion_kind("openai:responses:compact", "gemini:cli"),
request_conversion_kind("openai:responses:compact", "gemini:generate_content"),
None
);
assert_eq!(
request_conversion_kind("gemini:cli", "openai:responses:compact"),
request_conversion_kind("gemini:generate_content", "openai:responses:compact"),
None
);
assert_eq!(
@@ -317,14 +310,14 @@ mod tests {
None
);
assert_eq!(
request_conversion_kind("gemini:chat", "claude:chat"),
request_conversion_kind("gemini:generate_content", "claude:messages"),
Some(RequestConversionKind::ToClaudeStandard)
);
assert_eq!(request_conversion_kind("claude:chat", "claude:cli"), None);
assert_eq!(
request_conversion_kind("claude:chat", "claude:cli"),
Some(RequestConversionKind::ToClaudeStandard)
request_conversion_kind("claude:messages", "claude:messages"),
None
);
assert_eq!(request_conversion_kind("claude:chat", "claude:chat"), None);
}
#[test]
@@ -351,35 +344,35 @@ mod tests {
#[test]
fn sync_response_conversion_registry_supports_bidirectional_standard_matrix() {
assert_eq!(
sync_chat_response_conversion_kind("openai:chat", "claude:chat"),
sync_chat_response_conversion_kind("openai:chat", "claude:messages"),
Some(SyncChatResponseConversionKind::ToClaudeChat)
);
assert_eq!(
sync_chat_response_conversion_kind("claude:chat", "gemini:chat"),
sync_chat_response_conversion_kind("claude:messages", "gemini:generate_content"),
Some(SyncChatResponseConversionKind::ToGeminiChat)
);
assert_eq!(
sync_chat_response_conversion_kind("gemini:chat", "openai:chat"),
sync_chat_response_conversion_kind("gemini:generate_content", "openai:chat"),
Some(SyncChatResponseConversionKind::ToOpenAIChat)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses", "gemini:cli"),
sync_cli_response_conversion_kind("openai:responses", "gemini:generate_content"),
Some(SyncCliResponseConversionKind::ToGeminiCli)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:chat", "openai:responses"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("claude:cli", "openai:responses:compact"),
sync_cli_response_conversion_kind("claude:messages", "openai:responses:compact"),
Some(SyncCliResponseConversionKind::ToOpenAiResponses)
);
assert_eq!(
sync_cli_response_conversion_kind("openai:responses:compact", "claude:cli"),
sync_cli_response_conversion_kind("openai:responses:compact", "claude:messages"),
None
);
assert_eq!(
sync_cli_response_conversion_kind("gemini:cli", "claude:cli"),
sync_cli_response_conversion_kind("gemini:generate_content", "claude:messages"),
Some(SyncCliResponseConversionKind::ToClaudeCli)
);
assert_eq!(
@@ -395,7 +388,7 @@ mod tests {
#[test]
fn sync_response_conversion_registry_covers_all_standard_surface_pairs() {
for provider_api_format in STANDARD_SURFACES {
for client_api_format in ["openai:chat", "claude:chat", "gemini:chat"] {
for client_api_format in ["openai:chat", "claude:messages", "gemini:generate_content"] {
let actual =
sync_chat_response_conversion_kind(provider_api_format, client_api_format);
if *provider_api_format == client_api_format {
@@ -406,8 +399,8 @@ mod tests {
} else {
let expected = match client_api_format {
"openai:chat" => SyncChatResponseConversionKind::ToOpenAIChat,
"claude:chat" => SyncChatResponseConversionKind::ToClaudeChat,
"gemini:chat" => SyncChatResponseConversionKind::ToGeminiChat,
"claude:messages" => SyncChatResponseConversionKind::ToClaudeChat,
"gemini:generate_content" => SyncChatResponseConversionKind::ToGeminiChat,
other => panic!("unexpected chat client api format: {other}"),
};
assert_eq!(
@@ -418,7 +411,11 @@ mod tests {
}
}
for client_api_format in ["openai:responses", "claude:cli", "gemini:cli"] {
for client_api_format in [
"openai:responses",
"claude:messages",
"gemini:generate_content",
] {
let actual =
sync_cli_response_conversion_kind(provider_api_format, client_api_format);
if *provider_api_format == client_api_format {
@@ -429,8 +426,8 @@ mod tests {
} else {
let expected = match client_api_format {
"openai:responses" => SyncCliResponseConversionKind::ToOpenAiResponses,
"claude:cli" => SyncCliResponseConversionKind::ToClaudeCli,
"gemini:cli" => SyncCliResponseConversionKind::ToGeminiCli,
"claude:messages" => SyncCliResponseConversionKind::ToClaudeCli,
"gemini:generate_content" => SyncCliResponseConversionKind::ToGeminiCli,
other => panic!("unexpected cli client api format: {other}"),
};
assert_eq!(
@@ -449,49 +446,31 @@ mod tests {
request_candidate_api_formats("openai:chat", false),
vec![
"openai:chat",
"claude:chat",
"gemini:chat",
"openai:responses",
"claude:cli",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:responses", false),
vec![
"openai:responses",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
"claude:messages",
"gemini:generate_content",
]
);
assert_eq!(
request_candidate_api_formats("openai:cli", false),
vec![
"openai:responses",
"claude:cli",
"gemini:cli",
"openai:chat",
"claude:chat",
"gemini:chat",
]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("claude:cli", false),
vec![
"claude:cli",
"openai:responses",
"gemini:cli",
"claude:chat",
"openai:chat",
"gemini:chat",
]
Vec::<&'static str>::new()
);
assert_eq!(
request_candidate_api_formats("openai:compact", false),
vec!["openai:responses:compact"]
Vec::<&'static str>::new()
);
}
@@ -499,21 +478,25 @@ mod tests {
fn request_candidate_registry_prefers_same_kind_before_same_family_fallbacks() {
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:responses"),
Some((1, 0))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "claude:chat"),
Some((2, 1))
None
);
assert_eq!(
request_candidate_api_format_preference("claude:cli", "openai:chat"),
Some((3, 0))
None
);
}
#[test]
fn request_conversion_enable_flag_only_applies_to_real_data_format_conversions() {
assert!(!request_conversion_requires_enable_flag(
"claude:messages",
"claude:messages"
));
assert!(request_conversion_requires_enable_flag(
"claude:chat",
"claude:cli"
));
@@ -527,7 +510,7 @@ mod tests {
));
assert!(request_conversion_requires_enable_flag(
"openai:chat",
"gemini:chat"
"gemini:generate_content"
));
}
@@ -627,7 +610,7 @@ mod tests {
config: None,
format_acceptance_config: Some(serde_json::json!({
"enabled": true,
"accept_formats": ["claude:cli"],
"accept_formats": ["claude:messages"],
})),
proxy: None,
},
@@ -652,17 +635,17 @@ mod tests {
assert!(request_conversion_enabled_for_transport(
&transport,
"claude:cli",
"claude:messages",
"openai:responses"
));
assert!(request_pair_allowed_for_transport(
&transport,
"claude:cli",
"claude:messages",
"openai:responses"
));
assert!(!request_pair_allowed_for_transport(
&transport,
"gemini:cli",
"gemini:generate_content",
"openai:responses"
));
}
@@ -700,7 +683,7 @@ mod tests {
config: None,
format_acceptance_config: Some(serde_json::json!({
"enabled": true,
"reject_formats": ["claude:cli"],
"reject_formats": ["claude:messages"],
})),
proxy: None,
},
@@ -725,7 +708,7 @@ mod tests {
assert!(!request_conversion_enabled_for_transport(
&transport,
"claude:cli",
"claude:messages",
"openai:responses"
));
}
@@ -751,9 +734,9 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-vertex".to_string(),
provider_id: "provider-vertex".to_string(),
api_format: "gemini:chat".to_string(),
api_format: "gemini:generate_content".to_string(),
api_family: Some("gemini".to_string()),
endpoint_kind: Some("chat".to_string()),
endpoint_kind: Some("generate_content".to_string()),
is_active: true,
base_url: "https://aiplatform.googleapis.com".to_string(),
header_rules: None,
@@ -770,7 +753,7 @@ mod tests {
name: "key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:chat".to_string()]),
api_formats: Some(vec!["gemini:generate_content".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -794,7 +777,7 @@ mod tests {
}
#[test]
fn kiro_claude_cli_transport_supports_cross_format_conversion_via_envelope() {
fn kiro_claude_messages_transport_supports_cross_format_conversion_via_envelope() {
let transport = GatewayProviderTransportSnapshot {
provider: GatewayProviderTransportProvider {
id: "provider-kiro".to_string(),
@@ -814,9 +797,9 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-kiro".to_string(),
provider_id: "provider-kiro".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
endpoint_kind: Some("messages".to_string()),
is_active: true,
base_url: "https://q.{region}.amazonaws.com".to_string(),
header_rules: None,
@@ -833,7 +816,7 @@ mod tests {
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -849,7 +832,7 @@ mod tests {
assert!(request_pair_allowed_for_transport(
&transport,
"openai:chat",
"claude:cli"
"claude:messages"
));
assert!(request_conversion_transport_supported(
&transport,

View File

@@ -459,7 +459,7 @@ mod tests {
"\"code\":\"invalid_request\"",
),
(
"claude:chat",
"claude:messages",
data_line(json!({
"type": "error",
"error": {
@@ -473,7 +473,7 @@ mod tests {
"\"code\":\"rate_limit\"",
),
(
"gemini:cli",
"gemini:generate_content",
data_line(json!({
"error": {
"code": 429,
@@ -524,7 +524,7 @@ mod tests {
"\"code\":\"invalid_request\"",
),
(
"claude:chat",
"claude:messages",
data_line(json!({
"type": "error",
"error": {
@@ -538,7 +538,7 @@ mod tests {
"\"code\":\"rate_limit\"",
),
(
"gemini:cli",
"gemini:generate_content",
data_line(json!({
"error": {
"code": 429,
@@ -553,7 +553,7 @@ mod tests {
];
for (provider_api_format, line, message, err_type, code) in cases {
let report_context = report_context(provider_api_format, "claude:chat");
let report_context = report_context(provider_api_format, "claude:messages");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(&report_context, line)
@@ -591,7 +591,7 @@ mod tests {
"\"status\":\"INVALID_ARGUMENT\"",
),
(
"claude:chat",
"claude:messages",
data_line(json!({
"type": "error",
"error": {
@@ -605,7 +605,7 @@ mod tests {
"\"status\":\"RESOURCE_EXHAUSTED\"",
),
(
"gemini:cli",
"gemini:generate_content",
data_line(json!({
"error": {
"code": 429,
@@ -620,7 +620,7 @@ mod tests {
];
for (provider_api_format, line, message, code, status) in cases {
let report_context = report_context(provider_api_format, "gemini:chat");
let report_context = report_context(provider_api_format, "gemini:generate_content");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(&report_context, line)
@@ -656,7 +656,7 @@ mod tests {
"\"code\":\"invalid_request\"",
),
(
"claude:chat",
"claude:messages",
data_line(json!({
"type": "error",
"error": {
@@ -670,7 +670,7 @@ mod tests {
"\"code\":\"rate_limit\"",
),
(
"gemini:cli",
"gemini:generate_content",
data_line(json!({
"error": {
"code": 429,
@@ -706,7 +706,7 @@ mod tests {
#[test]
fn rewrites_gemini_inline_image_streams_to_claude_image_blocks() {
let report_context = report_context("gemini:chat", "claude:chat");
let report_context = report_context("gemini:generate_content", "claude:messages");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(
@@ -735,7 +735,7 @@ mod tests {
#[test]
fn rewrites_claude_image_blocks_to_gemini_inline_image_streams() {
let report_context = report_context("claude:chat", "gemini:chat");
let report_context = report_context("claude:messages", "gemini:generate_content");
let mut matrix = StreamingStandardFormatMatrix::default();
let output = matrix
.transform_line(
@@ -763,7 +763,7 @@ mod tests {
#[test]
fn terminal_observer_preserves_claude_cache_usage() {
let report_context = report_context("claude:chat", "openai:chat");
let report_context = report_context("claude:messages", "openai:chat");
let mut observer = StreamingStandardTerminalObserver::default();
observer

View File

@@ -41,7 +41,7 @@ pub fn resolve_finalize_stream_rewrite_mode(
if needs_conversion
&& envelope_name.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME)
&& provider_api_format == "claude:cli"
&& provider_api_format == "claude:messages"
{
return supports_standard_stream_rewrite(
provider_api_format.as_str(),
@@ -63,7 +63,8 @@ pub fn resolve_finalize_stream_rewrite_mode(
}
if envelope_name.eq_ignore_ascii_case(KIRO_ENVELOPE_NAME) {
return (provider_api_format == "claude:cli" && client_api_format == "claude:cli")
return (provider_api_format == "claude:messages"
&& client_api_format == "claude:messages")
.then_some(FinalizeStreamRewriteMode::KiroToClaudeCli);
}
@@ -83,25 +84,29 @@ fn supports_standard_stream_rewrite(provider_api_format: &str, client_api_format
fn is_standard_provider_api_format(api_format: &str) -> bool {
matches!(
aether_ai_formats::normalize_legacy_openai_format_alias(api_format).as_str(),
aether_ai_formats::normalize_api_format_alias(api_format).as_str(),
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
| "gemini:cli"
| "claude:messages"
| "gemini:generate_content"
)
}
fn is_standard_chat_client_api_format(api_format: &str) -> bool {
matches!(api_format, "openai:chat" | "claude:chat" | "gemini:chat")
matches!(
api_format,
"openai:chat" | "claude:messages" | "gemini:generate_content"
)
}
fn is_standard_cli_client_api_format(api_format: &str) -> bool {
matches!(
aether_ai_formats::normalize_legacy_openai_format_alias(api_format).as_str(),
"openai:responses" | "openai:responses:compact" | "claude:cli" | "gemini:cli"
aether_ai_formats::normalize_api_format_alias(api_format).as_str(),
"openai:responses"
| "openai:responses:compact"
| "claude:messages"
| "gemini:generate_content"
)
}
@@ -114,7 +119,7 @@ mod tests {
#[test]
fn resolves_standard_mode_for_cross_format_standard_streams() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"needs_conversion": true,
});
@@ -127,8 +132,8 @@ mod tests {
#[test]
fn resolves_envelope_unwrap_for_same_format_private_envelopes() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"envelope_name": "antigravity:v1internal",
"needs_conversion": false,
});
@@ -141,8 +146,8 @@ mod tests {
#[test]
fn resolves_kiro_same_format_streams_to_kiro_mode() {
let report_context = json!({
"provider_api_format": "claude:cli",
"client_api_format": "claude:cli",
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"envelope_name": "kiro:generateAssistantResponse",
"needs_conversion": false,
});

View File

@@ -176,10 +176,10 @@ pub fn maybe_build_openai_chat_cross_format_sync_product_from_normalized_payload
};
let Some(client_body_json) = (match provider_api_format.as_str() {
"claude:chat" | "claude:cli" => {
"claude:messages" => {
convert_claude_canonical_response_to_openai_chat(&provider_body_json, report_context)
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
convert_gemini_canonical_response_to_openai_chat(&provider_body_json, report_context)
}
"openai:responses" => {
@@ -248,10 +248,10 @@ pub fn maybe_build_openai_responses_cross_format_sync_product_from_normalized_pa
normalize_openai_responses_family_api_format(&provider_api_format);
let Some(client_body_json) = (match normalized_provider_api_format.as_str() {
"openai:responses" | "openai:responses:compact" => Some(provider_body_json.clone()),
"claude:chat" | "claude:cli" => {
"claude:messages" => {
convert_claude_response_to_openai_responses(&provider_body_json, report_context)
}
"gemini:chat" | "gemini:cli" => convert_gemini_canonical_response_to_openai_responses(
"gemini:generate_content" => convert_gemini_canonical_response_to_openai_responses(
&provider_body_json,
report_context,
normalized_client_api_format == "openai:responses:compact",
@@ -541,8 +541,8 @@ fn maybe_build_openai_cross_format_provider_body_from_normalized_payload(
let normalized_provider_api_format =
normalize_openai_responses_family_api_format(provider_api_format);
match normalized_provider_api_format.as_str() {
"claude:chat" | "claude:cli" => aggregate_claude_stream_sync_response(&body_bytes),
"gemini:chat" | "gemini:cli" => aggregate_gemini_stream_sync_response(&body_bytes),
"claude:messages" => aggregate_claude_stream_sync_response(&body_bytes),
"gemini:generate_content" => aggregate_gemini_stream_sync_response(&body_bytes),
"openai:responses" | "openai:responses:compact" => {
aggregate_openai_responses_stream_sync_response(&body_bytes)
}
@@ -622,13 +622,13 @@ pub fn aggregate_standard_chat_stream_sync_response(
body: &[u8],
provider_api_format: &str,
) -> Option<Value> {
match aether_ai_formats::normalize_legacy_openai_format_alias(provider_api_format).as_str() {
match aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str() {
"openai:chat" => aggregate_openai_chat_stream_sync_response(body),
"openai:responses" | "openai:responses:compact" => {
aggregate_openai_responses_stream_sync_response(body)
}
"claude:chat" | "claude:cli" => aggregate_claude_stream_sync_response(body),
"gemini:chat" | "gemini:cli" => aggregate_gemini_stream_sync_response(body),
"claude:messages" => aggregate_claude_stream_sync_response(body),
"gemini:generate_content" => aggregate_gemini_stream_sync_response(body),
_ => None,
}
}
@@ -660,7 +660,7 @@ pub fn convert_standard_chat_response(
}
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"claude:chat" | "claude:cli"
"claude:messages"
) {
return convert_claude_canonical_chat_response(
body_json,
@@ -670,7 +670,7 @@ pub fn convert_standard_chat_response(
}
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"gemini:chat" | "gemini:cli"
"gemini:generate_content"
) {
return convert_gemini_canonical_chat_response(
body_json,
@@ -725,13 +725,13 @@ pub fn convert_standard_cli_response(
}
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"claude:chat" | "claude:cli"
"claude:messages"
) {
return convert_claude_canonical_cli_response(body_json, client_api_format, report_context);
}
if matches!(
provider_api_format.trim().to_ascii_lowercase().as_str(),
"gemini:chat" | "gemini:cli"
"gemini:generate_content"
) {
return convert_gemini_canonical_cli_response(body_json, client_api_format, report_context);
}
@@ -760,8 +760,12 @@ pub fn convert_standard_cli_response(
"openai:responses:compact" => {
convert_openai_chat_response_to_openai_responses(&canonical, report_context, true)
}
"claude:cli" => convert_openai_chat_response_to_claude_chat(&canonical, report_context),
"gemini:cli" => convert_openai_chat_response_to_gemini_chat(&canonical, report_context),
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(&canonical, report_context)
}
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(&canonical, report_context)
}
_ => None,
}
}
@@ -787,19 +791,23 @@ fn convert_openai_chat_canonical_chat_response(
if !openai_chat_response_can_use_single_response_canonical(body_json) {
return match client_api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => Some(body_json.clone()),
"claude:chat" => convert_openai_chat_response_to_claude_chat(body_json, report_context),
"gemini:chat" => convert_openai_chat_response_to_gemini_chat(body_json, report_context),
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(body_json, report_context)
}
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(body_json, report_context)
}
_ => None,
};
}
match client_api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => Some(body_json.clone()),
"claude:chat" => {
"claude:messages" => {
let openai_chat =
convert_openai_chat_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:chat" => {
"gemini:generate_content" => {
let openai_chat =
convert_openai_chat_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
@@ -821,8 +829,12 @@ fn convert_openai_chat_canonical_cli_response(
"openai:responses:compact" => {
convert_openai_chat_response_to_openai_responses(body_json, report_context, true)
}
"claude:cli" => convert_openai_chat_response_to_claude_chat(body_json, report_context),
"gemini:cli" => convert_openai_chat_response_to_gemini_chat(body_json, report_context),
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(body_json, report_context)
}
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(body_json, report_context)
}
_ => None,
};
}
@@ -835,8 +847,12 @@ fn convert_openai_chat_canonical_cli_response(
"openai:responses:compact" => {
convert_openai_chat_response_to_openai_responses(&openai_chat, report_context, true)
}
"claude:cli" => convert_openai_chat_response_to_claude_chat(&openai_chat, report_context),
"gemini:cli" => convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context),
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
}
_ => None,
}
}
@@ -873,7 +889,7 @@ fn convert_openai_responses_canonical_chat_response(
.or_else(|| {
convert_openai_responses_response_to_openai_chat(body_json, report_context)
}),
"claude:chat" => canonical
"claude:messages" => canonical
.as_ref()
.map(canonical_to_claude_response)
.or_else(|| {
@@ -881,7 +897,7 @@ fn convert_openai_responses_canonical_chat_response(
convert_openai_responses_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}),
"gemini:chat" => canonical
"gemini:generate_content" => canonical
.as_ref()
.and_then(|canonical| canonical_to_gemini_response(canonical, report_context))
.or_else(|| {
@@ -921,7 +937,7 @@ fn convert_openai_responses_canonical_responses_response(
convert_openai_chat_response_to_openai_responses(&openai_chat, report_context, true)
})
}
"claude:cli" => {
"claude:messages" => {
let canonical =
from_openai_responses_to_canonical_response(body_json).map(|mut canonical| {
apply_report_context_model_fallback(&mut canonical.model, report_context);
@@ -938,7 +954,7 @@ fn convert_openai_responses_canonical_responses_response(
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
})
}
"gemini:cli" => {
"gemini:generate_content" => {
let canonical =
from_openai_responses_to_canonical_response(body_json).map(|mut canonical| {
apply_report_context_model_fallback(&mut canonical.model, report_context);
@@ -982,8 +998,8 @@ fn convert_claude_canonical_chat_response(
"openai:chat" => {
convert_claude_canonical_response_to_openai_chat(body_json, report_context)
}
"claude:chat" => convert_claude_canonical_response_to_claude(body_json, report_context),
"gemini:chat" => {
"claude:messages" => convert_claude_canonical_response_to_claude(body_json, report_context),
"gemini:generate_content" => {
let openai_chat =
convert_claude_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
@@ -1008,8 +1024,8 @@ fn convert_claude_canonical_cli_response(
convert_claude_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_openai_responses(&openai_chat, report_context, true)
}
"claude:cli" => convert_claude_canonical_response_to_claude(body_json, report_context),
"gemini:cli" => {
"claude:messages" => convert_claude_canonical_response_to_claude(body_json, report_context),
"gemini:generate_content" => {
let openai_chat =
convert_claude_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
@@ -1054,10 +1070,10 @@ fn convert_gemini_canonical_chat_response(
let openai_chat = convert_gemini_chat_response_to_openai_chat(body_json, report_context)?;
return match client_api_format.trim().to_ascii_lowercase().as_str() {
"openai:chat" => Some(openai_chat),
"claude:chat" => {
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:chat" => {
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
}
_ => None,
@@ -1067,12 +1083,14 @@ fn convert_gemini_canonical_chat_response(
"openai:chat" => {
convert_gemini_canonical_response_to_openai_chat(body_json, report_context)
}
"claude:chat" => {
"claude:messages" => {
let openai_chat =
convert_gemini_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:chat" => convert_gemini_canonical_response_to_gemini(body_json, report_context),
"gemini:generate_content" => {
convert_gemini_canonical_response_to_gemini(body_json, report_context)
}
_ => None,
}
}
@@ -1093,10 +1111,10 @@ fn convert_gemini_canonical_cli_response(
"openai:responses:compact" => {
convert_openai_chat_response_to_openai_responses(&openai_chat, report_context, true)
}
"claude:cli" => {
"claude:messages" => {
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:cli" => {
"gemini:generate_content" => {
convert_openai_chat_response_to_gemini_chat(&openai_chat, report_context)
}
_ => None,
@@ -1109,12 +1127,14 @@ fn convert_gemini_canonical_cli_response(
"openai:responses:compact" => {
convert_gemini_canonical_response_to_openai_responses(body_json, report_context, true)
}
"claude:cli" => {
"claude:messages" => {
let openai_chat =
convert_gemini_canonical_response_to_openai_chat(body_json, report_context)?;
convert_openai_chat_response_to_claude_chat(&openai_chat, report_context)
}
"gemini:cli" => convert_gemini_canonical_response_to_gemini(body_json, report_context),
"gemini:generate_content" => {
convert_gemini_canonical_response_to_gemini(body_json, report_context)
}
_ => None,
}
}
@@ -1230,10 +1250,10 @@ fn is_openai_responses_finalize_kind(report_kind: &str) -> bool {
fn standard_same_format_api_format(report_kind: &str) -> Option<&'static str> {
match report_kind {
"openai_chat_sync_finalize" => Some("openai:chat"),
"claude_chat_sync_finalize" => Some("claude:chat"),
"gemini_chat_sync_finalize" => Some("gemini:chat"),
"claude_cli_sync_finalize" => Some("claude:cli"),
"gemini_cli_sync_finalize" => Some("gemini:cli"),
"claude_chat_sync_finalize" => Some("claude:messages"),
"gemini_chat_sync_finalize" => Some("gemini:generate_content"),
"claude_cli_sync_finalize" => Some("claude:messages"),
"gemini_cli_sync_finalize" => Some("gemini:generate_content"),
_ => None,
}
}
@@ -1241,8 +1261,8 @@ fn standard_same_format_api_format(report_kind: &str) -> Option<&'static str> {
fn aggregate_same_format_stream_sync_response(api_format: &str, body: &[u8]) -> Option<Value> {
match api_format {
"openai:chat" => aggregate_openai_chat_stream_sync_response(body),
"claude:chat" | "claude:cli" => aggregate_claude_stream_sync_response(body),
"gemini:chat" | "gemini:cli" => aggregate_gemini_stream_sync_response(body),
"claude:messages" => aggregate_claude_stream_sync_response(body),
"gemini:generate_content" => aggregate_gemini_stream_sync_response(body),
_ => None,
}
}
@@ -1255,7 +1275,7 @@ fn is_openai_responses_family_api_format(api_format: &str) -> bool {
}
fn normalize_openai_responses_family_api_format(api_format: &str) -> String {
aether_ai_formats::normalize_legacy_openai_format_alias(api_format)
aether_ai_formats::normalize_api_format_alias(api_format)
}
fn parse_stream_json_events(body: &[u8]) -> Option<Vec<Value>> {
@@ -2945,7 +2965,7 @@ mod tests {
"data: {\"responseId\":\"resp_gem_stream_456\",\"modelVersion\":\"gemini-2.5-pro\",\"candidates\":[{\"index\":0,\"content\":{\"role\":\"model\",\"parts\":[]},\"finishReason\":\"STOP\"}],\"usageMetadata\":{\"promptTokenCount\":4,\"candidatesTokenCount\":2,\"totalTokenCount\":6}}\n\n",
);
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"mapped_model": "gemini-2.5-pro",
});
@@ -2992,7 +3012,7 @@ mod tests {
);
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
});
let product = maybe_build_standard_cross_format_sync_product_from_normalized_payload(
@@ -3049,7 +3069,7 @@ mod tests {
fn falls_back_to_body_json_when_stream_aggregation_returns_none() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
});
let provider_body_json = json!({
"id": "chatcmpl_123",
@@ -3103,8 +3123,8 @@ mod tests {
"data: {\"type\":\"message_stop\"}\n\n",
);
let report_context = json!({
"provider_api_format": "claude:chat",
"client_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"needs_conversion": false,
});
@@ -3129,8 +3149,8 @@ mod tests {
#[test]
fn falls_back_to_body_json_for_standard_same_format_sync_payload() {
let report_context = json!({
"provider_api_format": "gemini:cli",
"client_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "gemini:generate_content",
"needs_conversion": false,
});
let provider_body_json = json!({
@@ -3181,8 +3201,8 @@ mod tests {
#[test]
fn rejects_standard_same_format_error_body_json() {
let report_context = json!({
"provider_api_format": "claude:chat",
"client_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "claude:messages",
"needs_conversion": false,
});
let provider_body_json = json!({
@@ -3476,7 +3496,7 @@ mod tests {
#[test]
fn builds_openai_chat_cross_format_sync_product_from_claude_body_json() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "claude-sonnet-4",
@@ -3514,7 +3534,7 @@ mod tests {
#[test]
fn claude_canonical_response_route_matches_legacy_outputs_for_existing_clients() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "claude-sonnet-4",
@@ -3551,7 +3571,7 @@ mod tests {
.expect("legacy claude -> openai chat");
let converted_openai = convert_standard_chat_response(
&provider_body_json,
"claude:chat",
"claude:messages",
"openai:chat",
&report_context,
)
@@ -3563,8 +3583,8 @@ mod tests {
.expect("legacy claude -> openai chat -> claude");
let converted_claude = convert_standard_chat_response(
&provider_body_json,
"claude:chat",
"claude:chat",
"claude:messages",
"claude:messages",
&report_context,
)
.expect("canonical claude -> claude");
@@ -3578,7 +3598,7 @@ mod tests {
.expect("legacy claude -> openai responses");
let converted_openai_responses = convert_standard_cli_response(
&provider_body_json,
"claude:chat",
"claude:messages",
"openai:responses",
&report_context,
)
@@ -3589,7 +3609,7 @@ mod tests {
#[test]
fn gemini_canonical_response_route_matches_legacy_outputs_for_single_candidate() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro",
@@ -3628,7 +3648,7 @@ mod tests {
.expect("legacy gemini -> openai chat");
let converted_openai = convert_standard_chat_response(
&provider_body_json,
"gemini:chat",
"gemini:generate_content",
"openai:chat",
&report_context,
)
@@ -3640,8 +3660,8 @@ mod tests {
.expect("legacy gemini -> openai chat -> claude");
let converted_claude = convert_standard_chat_response(
&provider_body_json,
"gemini:chat",
"claude:chat",
"gemini:generate_content",
"claude:messages",
&report_context,
)
.expect("canonical gemini -> claude");
@@ -3655,7 +3675,7 @@ mod tests {
.expect("legacy gemini -> openai responses");
let converted_openai_responses = convert_standard_cli_response(
&provider_body_json,
"gemini:chat",
"gemini:generate_content",
"openai:responses",
&report_context,
)
@@ -3667,7 +3687,7 @@ mod tests {
fn openai_chat_canonical_response_route_matches_legacy_outputs_for_single_choice() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"model": "gpt-5",
"mapped_model": "gpt-5",
});
@@ -3712,7 +3732,7 @@ mod tests {
let converted_claude = convert_standard_chat_response(
&provider_body_json,
"openai:chat",
"claude:chat",
"claude:messages",
&report_context,
)
.expect("canonical openai chat -> claude");
@@ -3724,7 +3744,7 @@ mod tests {
let converted_gemini = convert_standard_chat_response(
&provider_body_json,
"openai:chat",
"gemini:chat",
"gemini:generate_content",
&report_context,
)
.expect("canonical openai chat -> gemini");
@@ -3818,7 +3838,7 @@ mod tests {
let converted_claude = convert_standard_chat_response(
&provider_body_json,
"openai:responses",
"claude:chat",
"claude:messages",
&report_context,
)
.expect("canonical openai responses -> claude");
@@ -3832,7 +3852,7 @@ mod tests {
let converted_gemini = convert_standard_chat_response(
&provider_body_json,
"openai:responses",
"gemini:chat",
"gemini:generate_content",
&report_context,
)
.expect("canonical openai responses -> gemini");
@@ -3866,7 +3886,7 @@ mod tests {
#[test]
fn builds_openai_chat_cross_format_sync_product_from_gemini_body_json() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro",
@@ -3957,7 +3977,7 @@ mod tests {
"data: {\"responseId\":\"resp_cli_stream_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Gemini CLI\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro-upstream\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
);
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro-upstream",
@@ -4155,7 +4175,7 @@ mod tests {
#[test]
fn standard_sync_finalize_product_handles_openai_chat_cross_format() {
let report_context = json!({
"provider_api_format": "claude:chat",
"provider_api_format": "claude:messages",
"client_api_format": "openai:chat",
"model": "gpt-5",
"mapped_model": "claude-sonnet-4",
@@ -4192,7 +4212,7 @@ mod tests {
"data: {\"responseId\":\"resp_cli_stream_123\",\"candidates\":[{\"content\":{\"parts\":[{\"text\":\"Hello Gemini CLI\"}],\"role\":\"model\"},\"finishReason\":\"STOP\",\"index\":0}],\"modelVersion\":\"gemini-2.5-pro-upstream\",\"usageMetadata\":{\"promptTokenCount\":2,\"candidatesTokenCount\":3,\"totalTokenCount\":5}}\n\n",
);
let report_context = json!({
"provider_api_format": "gemini:cli",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:responses",
"model": "gpt-5",
"mapped_model": "gemini-2.5-pro-upstream",
@@ -4218,7 +4238,7 @@ mod tests {
fn standard_sync_finalize_product_falls_back_to_generic_standard_cross_format() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
});
let provider_body_json = json!({
"id": "chatcmpl_123",
@@ -4255,7 +4275,7 @@ mod tests {
fn multi_choice_openai_chat_response_keeps_legacy_fallback_route() {
let report_context = json!({
"provider_api_format": "openai:chat",
"client_api_format": "claude:chat",
"client_api_format": "claude:messages",
"model": "gpt-5",
"mapped_model": "gpt-5-upstream",
});
@@ -4288,7 +4308,7 @@ mod tests {
let converted = convert_standard_chat_response(
&provider_body_json,
"openai:chat",
"claude:chat",
"claude:messages",
&report_context,
)
.expect("fallback route");
@@ -4298,7 +4318,7 @@ mod tests {
#[test]
fn multi_candidate_gemini_response_keeps_legacy_fallback_route() {
let report_context = json!({
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"client_api_format": "openai:chat",
"model": "gemini-2.5-pro",
"mapped_model": "gemini-upstream",
@@ -4330,7 +4350,7 @@ mod tests {
.expect("legacy multi candidate route");
let converted = convert_standard_chat_response(
&provider_body_json,
"gemini:chat",
"gemini:generate_content",
"openai:chat",
&report_context,
)

View File

@@ -60,18 +60,21 @@ mod tests {
"codex",
"openai:responses"
));
assert!(force_upstream_streaming_for_provider("codex", "openai:cli"));
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:responses:compact"
));
}
#[test]
fn does_not_force_streaming_for_compact_or_other_provider_types() {
assert!(!force_upstream_streaming_for_provider(
"codex",
"openai:compact"
"openai:responses:compact"
));
assert!(!force_upstream_streaming_for_provider(
"openai",
"openai:cli"
"openai:responses"
));
}
}

View File

@@ -22,28 +22,28 @@ pub struct LocalSameFormatProviderSpec {
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
match plan_kind {
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:chat",
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND,
report_kind: "claude_chat_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:cli",
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_SYNC_PLAN_KIND,
report_kind: "claude_cli_sync_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: false,
}),
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:chat",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
report_kind: "gemini_chat_sync_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: false,
}),
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:cli",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
report_kind: "gemini_cli_sync_success",
family: LocalSameFormatProviderFamily::Gemini,
@@ -56,28 +56,28 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec>
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalSameFormatProviderSpec> {
match plan_kind {
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:chat",
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
report_kind: "claude_chat_stream_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: true,
}),
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "claude:cli",
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_STREAM_PLAN_KIND,
report_kind: "claude_cli_stream_success",
family: LocalSameFormatProviderFamily::Standard,
require_streaming: true,
}),
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:chat",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
report_kind: "gemini_chat_stream_success",
family: LocalSameFormatProviderFamily::Gemini,
require_streaming: true,
}),
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalSameFormatProviderSpec {
api_format: "gemini:cli",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
report_kind: "gemini_cli_stream_success",
family: LocalSameFormatProviderFamily::Gemini,
@@ -94,7 +94,7 @@ mod tests {
#[test]
fn resolves_claude_sync_same_format_spec() {
let spec = resolve_sync_spec("claude_chat_sync").expect("spec");
assert_eq!(spec.api_format, "claude:chat");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_chat_sync_success");
assert!(!spec.require_streaming);
}
@@ -102,7 +102,7 @@ mod tests {
#[test]
fn resolves_gemini_stream_same_format_spec() {
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
assert_eq!(spec.api_format, "gemini:cli");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
assert!(spec.require_streaming);
}

View File

@@ -43,7 +43,7 @@ pub fn resolve_execution_runtime_stream_plan_kind(
}
if route_family == Some("claude")
&& route_kind == Some("chat")
&& is_claude_messages_route_kind(route_kind)
&& *method == Method::POST
&& path == "/v1/messages"
{
@@ -59,7 +59,7 @@ pub fn resolve_execution_runtime_stream_plan_kind(
}
if route_family == Some("gemini")
&& route_kind == Some("chat")
&& is_gemini_generate_content_route_kind(route_kind)
&& *method == Method::POST
&& path.ends_with(":streamGenerateContent")
{
@@ -206,7 +206,7 @@ pub fn resolve_execution_runtime_sync_plan_kind(
}
if route_family == Some("claude")
&& route_kind == Some("chat")
&& is_claude_messages_route_kind(route_kind)
&& *method == Method::POST
&& path == "/v1/messages"
{
@@ -222,7 +222,7 @@ pub fn resolve_execution_runtime_sync_plan_kind(
}
if route_family == Some("gemini")
&& route_kind == Some("chat")
&& is_gemini_generate_content_route_kind(route_kind)
&& *method == Method::POST
&& path.ends_with(":generateContent")
{
@@ -269,6 +269,14 @@ fn is_openai_responses_compact_route_kind(route_kind: Option<&str>) -> bool {
matches!(route_kind, Some("responses:compact") | Some("compact"))
}
fn is_claude_messages_route_kind(route_kind: Option<&str>) -> bool {
matches!(route_kind, Some("messages") | Some("chat"))
}
fn is_gemini_generate_content_route_kind(route_kind: Option<&str>) -> bool {
matches!(route_kind, Some("generate_content") | Some("chat"))
}
pub fn is_matching_stream_request(
plan_kind: &str,
path: &str,

View File

@@ -6,7 +6,7 @@ use crate::planner::standard::family::{
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
CLAUDE_CHAT_SYNC_PLAN_KIND => Some(LocalStandardSpec {
api_format: "claude:chat",
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_SYNC_PLAN_KIND,
report_kind: "claude_chat_sync_finalize",
family: LocalStandardSourceFamily::Standard,
@@ -20,7 +20,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
CLAUDE_CHAT_STREAM_PLAN_KIND => Some(LocalStandardSpec {
api_format: "claude:chat",
api_format: "claude:messages",
decision_kind: CLAUDE_CHAT_STREAM_PLAN_KIND,
report_kind: "claude_chat_stream_success",
family: LocalStandardSourceFamily::Standard,
@@ -38,7 +38,7 @@ mod tests {
#[test]
fn resolves_claude_chat_sync_spec() {
let spec = resolve_sync_spec("claude_chat_sync").expect("spec");
assert_eq!(spec.api_format, "claude:chat");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_chat_sync_finalize");
assert!(!spec.require_streaming);
}
@@ -46,7 +46,7 @@ mod tests {
#[test]
fn resolves_claude_chat_stream_spec() {
let spec = resolve_stream_spec("claude_chat_stream").expect("spec");
assert_eq!(spec.api_format, "claude:chat");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_chat_stream_success");
assert!(spec.require_streaming);
}

View File

@@ -6,7 +6,7 @@ use crate::planner::standard::family::{
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
CLAUDE_CLI_SYNC_PLAN_KIND => Some(LocalStandardSpec {
api_format: "claude:cli",
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_SYNC_PLAN_KIND,
report_kind: "claude_cli_sync_finalize",
family: LocalStandardSourceFamily::Standard,
@@ -20,7 +20,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
CLAUDE_CLI_STREAM_PLAN_KIND => Some(LocalStandardSpec {
api_format: "claude:cli",
api_format: "claude:messages",
decision_kind: CLAUDE_CLI_STREAM_PLAN_KIND,
report_kind: "claude_cli_stream_success",
family: LocalStandardSourceFamily::Standard,
@@ -38,7 +38,7 @@ mod tests {
#[test]
fn resolves_claude_cli_sync_spec() {
let spec = resolve_sync_spec("claude_cli_sync").expect("spec");
assert_eq!(spec.api_format, "claude:cli");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_cli_sync_finalize");
assert!(!spec.require_streaming);
}
@@ -46,7 +46,7 @@ mod tests {
#[test]
fn resolves_claude_cli_stream_spec() {
let spec = resolve_stream_spec("claude_cli_stream").expect("spec");
assert_eq!(spec.api_format, "claude:cli");
assert_eq!(spec.api_format, "claude:messages");
assert_eq!(spec.report_kind, "claude_cli_stream_success");
assert!(spec.require_streaming);
}

View File

@@ -6,7 +6,7 @@ use crate::planner::standard::family::{
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
GEMINI_CHAT_SYNC_PLAN_KIND => Some(LocalStandardSpec {
api_format: "gemini:chat",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_SYNC_PLAN_KIND,
report_kind: "gemini_chat_sync_finalize",
family: LocalStandardSourceFamily::Gemini,
@@ -20,7 +20,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
GEMINI_CHAT_STREAM_PLAN_KIND => Some(LocalStandardSpec {
api_format: "gemini:chat",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CHAT_STREAM_PLAN_KIND,
report_kind: "gemini_chat_stream_success",
family: LocalStandardSourceFamily::Gemini,
@@ -38,7 +38,7 @@ mod tests {
#[test]
fn resolves_gemini_chat_sync_spec() {
let spec = resolve_sync_spec("gemini_chat_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:chat");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_chat_sync_finalize");
assert!(!spec.require_streaming);
}
@@ -46,7 +46,7 @@ mod tests {
#[test]
fn resolves_gemini_chat_stream_spec() {
let spec = resolve_stream_spec("gemini_chat_stream").expect("spec");
assert_eq!(spec.api_format, "gemini:chat");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_chat_stream_success");
assert!(spec.require_streaming);
}

View File

@@ -6,7 +6,7 @@ use crate::planner::standard::family::{
pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
GEMINI_CLI_SYNC_PLAN_KIND => Some(LocalStandardSpec {
api_format: "gemini:cli",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_SYNC_PLAN_KIND,
report_kind: "gemini_cli_sync_finalize",
family: LocalStandardSourceFamily::Gemini,
@@ -20,7 +20,7 @@ pub fn resolve_sync_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
pub fn resolve_stream_spec(plan_kind: &str) -> Option<LocalStandardSpec> {
match plan_kind {
GEMINI_CLI_STREAM_PLAN_KIND => Some(LocalStandardSpec {
api_format: "gemini:cli",
api_format: "gemini:generate_content",
decision_kind: GEMINI_CLI_STREAM_PLAN_KIND,
report_kind: "gemini_cli_stream_success",
family: LocalStandardSourceFamily::Gemini,
@@ -38,7 +38,7 @@ mod tests {
#[test]
fn resolves_gemini_cli_sync_spec() {
let spec = resolve_sync_spec("gemini_cli_sync").expect("spec");
assert_eq!(spec.api_format, "gemini:cli");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_cli_sync_finalize");
assert!(!spec.require_streaming);
}
@@ -46,7 +46,7 @@ mod tests {
#[test]
fn resolves_gemini_cli_stream_spec() {
let spec = resolve_stream_spec("gemini_cli_stream").expect("spec");
assert_eq!(spec.api_format, "gemini:cli");
assert_eq!(spec.api_format, "gemini:generate_content");
assert_eq!(spec.report_kind, "gemini_cli_stream_success");
assert!(spec.require_streaming);
}

View File

@@ -67,7 +67,7 @@ pub fn build_standard_request_body_from_canonical(
provider_api_format: &str,
upstream_is_stream: bool,
) -> Option<Value> {
match aether_ai_formats::normalize_legacy_openai_format_alias(provider_api_format).as_str() {
match aether_ai_formats::normalize_api_format_alias(provider_api_format).as_str() {
"openai:chat" => build_local_openai_chat_request_body(
canonical_request,
mapped_model,
@@ -85,12 +85,12 @@ pub fn build_standard_request_body_from_canonical(
false,
true,
),
"claude:chat" | "claude:cli" => convert_openai_chat_request_to_claude_request(
"claude:messages" => convert_openai_chat_request_to_claude_request(
canonical_request,
mapped_model,
upstream_is_stream,
),
"gemini:chat" | "gemini:cli" => convert_openai_chat_request_to_gemini_request(
"gemini:generate_content" => convert_openai_chat_request_to_gemini_request(
canonical_request,
mapped_model,
upstream_is_stream,
@@ -117,15 +117,15 @@ fn normalize_standard_request_to_openai_chat_request_cow<'a>(
client_api_format: &str,
request_path: &str,
) -> Option<Cow<'a, Value>> {
match aether_ai_formats::normalize_legacy_openai_format_alias(client_api_format).as_str() {
match aether_ai_formats::normalize_api_format_alias(client_api_format).as_str() {
"openai:chat" => Some(Cow::Borrowed(body_json)),
"openai:responses" | "openai:responses:compact" => {
normalize_openai_responses_request_to_openai_chat_request(body_json).map(Cow::Owned)
}
"claude:chat" | "claude:cli" => {
"claude:messages" => {
normalize_claude_request_to_openai_chat_request(body_json).map(Cow::Owned)
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
normalize_gemini_request_to_openai_chat_request(body_json, request_path).map(Cow::Owned)
}
_ => None,
@@ -162,10 +162,8 @@ mod tests {
const STANDARD_SURFACES: &[&str] = &[
"openai:chat",
"openai:responses",
"claude:chat",
"claude:cli",
"gemini:chat",
"gemini:cli",
"claude:messages",
"gemini:generate_content",
];
fn sample_request_for(api_format: &str) -> (Value, &'static str) {
@@ -190,7 +188,7 @@ mod tests {
}),
"/v1/responses",
),
"claude:chat" | "claude:cli" => (
"claude:messages" => (
json!({
"model": "source-model",
"system": "Be concise.",
@@ -202,7 +200,7 @@ mod tests {
}),
"/v1/messages",
),
"gemini:chat" | "gemini:cli" => (
"gemini:generate_content" => (
json!({
"systemInstruction": {
"parts": [{"text": "Be concise."}]
@@ -223,7 +221,7 @@ mod tests {
fn assert_stream_flag(provider_api_format: &str, upstream_is_stream: bool, converted: &Value) {
match provider_api_format {
"openai:chat" | "openai:responses" | "claude:chat" | "claude:cli" => {
"openai:chat" | "openai:responses" | "claude:messages" => {
assert_eq!(
converted
.get("stream")
@@ -233,7 +231,7 @@ mod tests {
"{provider_api_format} stream flag should follow upstream_is_stream"
);
}
"gemini:chat" | "gemini:cli" => {
"gemini:generate_content" => {
assert!(
converted.get("stream").is_none(),
"gemini streaming is represented by endpoint URL, not request body"
@@ -299,7 +297,7 @@ mod tests {
) -> Value {
let chat_canonical = normalize_standard_request_to_openai_chat_request(
request,
"claude:chat",
"claude:messages",
"/v1/messages",
)
.expect("legacy claude normalization should succeed");
@@ -319,7 +317,7 @@ mod tests {
) -> Value {
let chat_canonical = normalize_standard_request_to_openai_chat_request(
request,
"gemini:chat",
"gemini:generate_content",
"/v1beta/models/source-model:generateContent",
)
.expect("legacy gemini normalization should succeed");
@@ -607,13 +605,12 @@ mod tests {
"openai:chat",
"openai:responses",
"openai:responses:compact",
"gemini:chat",
"gemini:cli",
"gemini:generate_content",
] {
for upstream_is_stream in [false, true] {
let converted = build_standard_request_body(
&request,
"claude:chat",
"claude:messages",
"mapped-model",
"custom",
provider_api_format,
@@ -627,7 +624,7 @@ mod tests {
legacy_claude_request_body(&request, provider_api_format, upstream_is_stream);
assert_eq!(
converted, legacy,
"typed canonical claude:chat -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
"typed canonical claude:messages -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
);
}
}
@@ -689,13 +686,12 @@ mod tests {
"openai:chat",
"openai:responses",
"openai:responses:compact",
"claude:chat",
"claude:cli",
"claude:messages",
] {
for upstream_is_stream in [false, true] {
let converted = build_standard_request_body(
&request,
"gemini:chat",
"gemini:generate_content",
"mapped-model",
"custom",
provider_api_format,
@@ -709,7 +705,7 @@ mod tests {
legacy_gemini_request_body(&request, provider_api_format, upstream_is_stream);
assert_eq!(
converted, legacy,
"typed canonical gemini:chat -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
"typed canonical gemini:generate_content -> {provider_api_format} changed payload with upstream_is_stream={upstream_is_stream}"
);
}
}
@@ -770,7 +766,7 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:chat",
"claude:messages",
"gpt-5",
"openai",
"openai:chat",
@@ -801,7 +797,7 @@ mod tests {
let converted = build_standard_request_body(
&request,
"gemini:chat",
"gemini:generate_content",
"gpt-5",
"openai",
"openai:chat",
@@ -835,10 +831,10 @@ mod tests {
let converted = build_standard_request_body(
&request,
"gemini:chat",
"gemini:generate_content",
"claude-sonnet-4-5",
"anthropic",
"claude:chat",
"claude:messages",
"/v1beta/models/gemini-2.5-pro:generateContent",
false,
None,
@@ -871,10 +867,10 @@ mod tests {
let converted = build_standard_request_body(
&request,
"claude:cli",
"claude:messages",
"gemini-2.5-pro",
"google",
"gemini:cli",
"gemini:generate_content",
"/v1/messages",
false,
None,
@@ -1000,7 +996,7 @@ mod tests {
"openai:chat",
"gemini-2.5-pro",
"google",
"gemini:chat",
"gemini:generate_content",
"/v1/chat/completions",
false,
None,
@@ -1052,7 +1048,7 @@ mod tests {
"openai:chat",
"claude-sonnet-4-5",
"anthropic",
"claude:chat",
"claude:messages",
"/v1/chat/completions",
false,
None,
@@ -1073,7 +1069,7 @@ mod tests {
}
#[test]
fn openai_responses_nested_tools_survive_claude_cli_and_kiro_envelope_conversion() {
fn openai_responses_nested_tools_survive_claude_messages_and_kiro_envelope_conversion() {
let request = json!({
"model": "gpt-5",
"input": "Use the weather tool for Shanghai.",
@@ -1102,13 +1098,13 @@ mod tests {
"openai:responses",
"claude-sonnet-4.6",
"kiro",
"claude:cli",
"claude:messages",
"/v1/responses",
true,
None,
None,
)
.expect("openai responses should convert to claude cli");
.expect("openai responses should convert to claude messages");
assert_eq!(claude["tools"][0]["name"], "get_weather");
assert_eq!(claude["tool_choice"]["name"], "get_weather");

View File

@@ -87,7 +87,7 @@ mod tests {
80
);
assert_eq!(
normalize_input_tokens_for_billing(Some("gemini:chat"), 100, 20),
normalize_input_tokens_for_billing(Some("gemini:generate_content"), 100, 20),
80
);
}
@@ -95,7 +95,7 @@ mod tests {
#[test]
fn keeps_input_tokens_for_claude() {
assert_eq!(
normalize_input_tokens_for_billing(Some("claude:chat"), 100, 20),
normalize_input_tokens_for_billing(Some("claude:messages"), 100, 20),
100
);
}
@@ -107,7 +107,12 @@ mod tests {
120
);
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(Some("gemini:chat"), 120, 10, 15),
normalize_total_input_context_for_cache_hit_rate(
Some("gemini:generate_content"),
120,
10,
15
),
120
);
}
@@ -115,7 +120,7 @@ mod tests {
#[test]
fn includes_cache_creation_for_claude_cache_hit_context() {
assert_eq!(
normalize_total_input_context_for_cache_hit_rate(Some("claude:chat"), 60, 15, 5),
normalize_total_input_context_for_cache_hit_rate(Some("claude:messages"), 60, 15, 5),
80
);
}

View File

@@ -57,12 +57,8 @@ impl StoredMinimalCandidateSelectionRow {
}
}
fn normalize_api_format(value: &str) -> String {
aether_ai_formats::normalize_legacy_openai_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format(left) == normalize_api_format(right)
aether_ai_formats::api_format_alias_matches(left, right)
}
#[async_trait]

View File

@@ -0,0 +1,361 @@
CREATE OR REPLACE FUNCTION public.aether_canonical_api_format_alias(value text)
RETURNS text
LANGUAGE sql
IMMUTABLE
AS $$
SELECT CASE LOWER(BTRIM(COALESCE(value, '')))
WHEN 'openai:cli' THEN 'openai:responses'
WHEN 'openai:compact' THEN 'openai:responses:compact'
WHEN 'claude:chat' THEN 'claude:messages'
WHEN 'claude:cli' THEN 'claude:messages'
WHEN 'gemini:chat' THEN 'gemini:generate_content'
WHEN 'gemini:cli' THEN 'gemini:generate_content'
ELSE LOWER(BTRIM(COALESCE(value, '')))
END
$$;
DO $$
DECLARE
conflict_summary text;
BEGIN
SELECT string_agg(
DISTINCT provider_id::text || ':' || canonical_api_format,
', ' ORDER BY provider_id::text || ':' || canonical_api_format
)
INTO conflict_summary
FROM (
SELECT
left_endpoint.provider_id,
public.aether_canonical_api_format_alias(left_endpoint.api_format) AS canonical_api_format
FROM public.provider_endpoints AS left_endpoint
INNER JOIN public.provider_endpoints AS right_endpoint
ON right_endpoint.provider_id = left_endpoint.provider_id
AND right_endpoint.id > left_endpoint.id
AND public.aether_canonical_api_format_alias(right_endpoint.api_format)
= public.aether_canonical_api_format_alias(left_endpoint.api_format)
WHERE left_endpoint.api_format IN ('openai:responses', 'openai:cli', 'openai:responses:compact', 'openai:compact', 'claude:messages', 'claude:chat', 'claude:cli', 'gemini:generate_content', 'gemini:chat', 'gemini:cli')
AND right_endpoint.api_format IN ('openai:responses', 'openai:cli', 'openai:responses:compact', 'openai:compact', 'claude:messages', 'claude:chat', 'claude:cli', 'gemini:generate_content', 'gemini:chat', 'gemini:cli')
AND (
left_endpoint.base_url IS DISTINCT FROM right_endpoint.base_url
OR left_endpoint.custom_path IS DISTINCT FROM right_endpoint.custom_path
OR left_endpoint.max_retries IS DISTINCT FROM right_endpoint.max_retries
OR left_endpoint.header_rules::jsonb IS DISTINCT FROM right_endpoint.header_rules::jsonb
OR left_endpoint.body_rules::jsonb IS DISTINCT FROM right_endpoint.body_rules::jsonb
OR left_endpoint.config::jsonb IS DISTINCT FROM right_endpoint.config::jsonb
OR left_endpoint.proxy IS DISTINCT FROM right_endpoint.proxy
OR left_endpoint.format_acceptance_config::jsonb IS DISTINCT FROM right_endpoint.format_acceptance_config::jsonb
)
) AS conflicts;
IF conflict_summary IS NOT NULL THEN
RAISE EXCEPTION
'Cannot normalize OpenAI/Claude/Gemini provider_endpoints because transport fields differ for: %',
conflict_summary;
END IF;
END $$;
WITH grouped AS (
SELECT
id,
provider_id,
api_format,
public.aether_canonical_api_format_alias(api_format) AS canonical_api_format,
ROW_NUMBER() OVER (
PARTITION BY provider_id, public.aether_canonical_api_format_alias(api_format)
ORDER BY
CASE
WHEN api_format = public.aether_canonical_api_format_alias(api_format) THEN 0
ELSE 1
END,
created_at ASC,
id ASC
) AS rank
FROM public.provider_endpoints
WHERE api_format IN ('openai:responses', 'openai:cli', 'openai:responses:compact', 'openai:compact', 'claude:messages', 'claude:chat', 'claude:cli', 'gemini:generate_content', 'gemini:chat', 'gemini:cli')
),
survivors AS (
SELECT *
FROM grouped
WHERE rank = 1
),
retired AS (
UPDATE public.provider_endpoints AS endpoint
SET
is_active = FALSE,
updated_at = NOW()
FROM grouped
WHERE endpoint.id = grouped.id
AND grouped.rank > 1
RETURNING endpoint.id
)
UPDATE public.provider_endpoints AS endpoint
SET
api_format = survivors.canonical_api_format,
api_family = SPLIT_PART(survivors.canonical_api_format, ':', 1),
endpoint_kind = SUBSTRING(survivors.canonical_api_format FROM POSITION(':' IN survivors.canonical_api_format) + 1),
updated_at = NOW()
FROM survivors
WHERE endpoint.id = survivors.id
AND endpoint.api_format IS DISTINCT FROM survivors.canonical_api_format;
WITH expanded AS (
SELECT
pak.id,
formats.ordinality,
public.aether_canonical_api_format_alias(formats.value) AS api_format
FROM public.provider_api_keys AS pak
CROSS JOIN LATERAL json_array_elements_text(
CASE
WHEN json_typeof(pak.api_formats) = 'array' THEN pak.api_formats
ELSE '[]'::json
END
) WITH ORDINALITY AS formats(value, ordinality)
WHERE pak.api_formats IS NOT NULL
),
deduped AS (
SELECT id, api_format, MIN(ordinality) AS first_ordinality
FROM expanded
WHERE api_format <> ''
GROUP BY id, api_format
),
rebuilt AS (
SELECT id, json_agg(api_format ORDER BY first_ordinality) AS api_formats
FROM deduped
GROUP BY id
)
UPDATE public.provider_api_keys AS pak
SET
api_formats = rebuilt.api_formats,
updated_at = NOW()
FROM rebuilt
WHERE pak.id = rebuilt.id
AND pak.api_formats::jsonb IS DISTINCT FROM rebuilt.api_formats::jsonb;
WITH expanded AS (
SELECT
key.id,
formats.ordinality,
public.aether_canonical_api_format_alias(formats.value) AS api_format
FROM public.api_keys AS key
CROSS JOIN LATERAL json_array_elements_text(
CASE
WHEN json_typeof(key.allowed_api_formats) = 'array' THEN key.allowed_api_formats
ELSE '[]'::json
END
) WITH ORDINALITY AS formats(value, ordinality)
WHERE key.allowed_api_formats IS NOT NULL
),
deduped AS (
SELECT id, api_format, MIN(ordinality) AS first_ordinality
FROM expanded
WHERE api_format <> ''
GROUP BY id, api_format
),
rebuilt AS (
SELECT id, json_agg(api_format ORDER BY first_ordinality) AS allowed_api_formats
FROM deduped
GROUP BY id
)
UPDATE public.api_keys AS key
SET
allowed_api_formats = rebuilt.allowed_api_formats,
updated_at = NOW()
FROM rebuilt
WHERE key.id = rebuilt.id
AND key.allowed_api_formats::jsonb IS DISTINCT FROM rebuilt.allowed_api_formats::jsonb;
WITH expanded AS (
SELECT
users.id,
formats.ordinality,
public.aether_canonical_api_format_alias(formats.value) AS api_format
FROM public.users AS users
CROSS JOIN LATERAL json_array_elements_text(
CASE
WHEN json_typeof(users.allowed_api_formats) = 'array' THEN users.allowed_api_formats
ELSE '[]'::json
END
) WITH ORDINALITY AS formats(value, ordinality)
WHERE users.allowed_api_formats IS NOT NULL
),
deduped AS (
SELECT id, api_format, MIN(ordinality) AS first_ordinality
FROM expanded
WHERE api_format <> ''
GROUP BY id, api_format
),
rebuilt AS (
SELECT id, json_agg(api_format ORDER BY first_ordinality) AS allowed_api_formats
FROM deduped
GROUP BY id
)
UPDATE public.users AS users
SET
allowed_api_formats = rebuilt.allowed_api_formats,
updated_at = NOW()
FROM rebuilt
WHERE users.id = rebuilt.id
AND users.allowed_api_formats::jsonb IS DISTINCT FROM rebuilt.allowed_api_formats::jsonb;
WITH mapping_items AS (
SELECT
models.id,
item.ordinality AS item_ordinality,
item.value AS item
FROM public.models AS models
CROSS JOIN LATERAL jsonb_array_elements(
CASE
WHEN jsonb_typeof(models.provider_model_mappings) = 'array' THEN models.provider_model_mappings
ELSE '[]'::jsonb
END
) WITH ORDINALITY AS item(value, ordinality)
WHERE models.provider_model_mappings IS NOT NULL
),
rebuilt_items AS (
SELECT
id,
item_ordinality,
CASE
WHEN jsonb_typeof(item) = 'object'
AND jsonb_typeof(item->'api_formats') = 'array'
THEN jsonb_set(
item,
'{api_formats}',
COALESCE(
(
SELECT jsonb_agg(api_format ORDER BY first_ordinality)
FROM (
SELECT
public.aether_canonical_api_format_alias(format.value) AS api_format,
MIN(format.ordinality) AS first_ordinality
FROM jsonb_array_elements_text(item->'api_formats') WITH ORDINALITY AS format(value, ordinality)
GROUP BY public.aether_canonical_api_format_alias(format.value)
) AS deduped_formats
WHERE api_format <> ''
),
'[]'::jsonb
),
true
)
ELSE item
END AS item
FROM mapping_items
),
rebuilt AS (
SELECT id, jsonb_agg(item ORDER BY item_ordinality) AS provider_model_mappings
FROM rebuilt_items
GROUP BY id
)
UPDATE public.models AS models
SET
provider_model_mappings = rebuilt.provider_model_mappings,
updated_at = NOW()
FROM rebuilt
WHERE models.id = rebuilt.id
AND models.provider_model_mappings IS DISTINCT FROM rebuilt.provider_model_mappings;
WITH rebuilt AS (
SELECT
pak.id,
jsonb_object_agg(
public.aether_canonical_api_format_alias(entry.key),
entry.value
ORDER BY entry.ordinality
) AS rate_multipliers
FROM public.provider_api_keys AS pak
CROSS JOIN LATERAL json_each(
CASE
WHEN json_typeof(pak.rate_multipliers) = 'object' THEN pak.rate_multipliers
ELSE '{}'::json
END
) WITH ORDINALITY AS entry(key, value, ordinality)
WHERE pak.rate_multipliers IS NOT NULL
GROUP BY pak.id
)
UPDATE public.provider_api_keys AS pak
SET
rate_multipliers = rebuilt.rate_multipliers::json,
updated_at = NOW()
FROM rebuilt
WHERE pak.id = rebuilt.id
AND pak.rate_multipliers::jsonb IS DISTINCT FROM rebuilt.rate_multipliers;
WITH rebuilt AS (
SELECT
pak.id,
jsonb_object_agg(
public.aether_canonical_api_format_alias(entry.key),
entry.value
ORDER BY entry.ordinality
) AS global_priority_by_format
FROM public.provider_api_keys AS pak
CROSS JOIN LATERAL json_each(
CASE
WHEN json_typeof(pak.global_priority_by_format) = 'object' THEN pak.global_priority_by_format
ELSE '{}'::json
END
) WITH ORDINALITY AS entry(key, value, ordinality)
WHERE pak.global_priority_by_format IS NOT NULL
GROUP BY pak.id
)
UPDATE public.provider_api_keys AS pak
SET
global_priority_by_format = rebuilt.global_priority_by_format::json,
updated_at = NOW()
FROM rebuilt
WHERE pak.id = rebuilt.id
AND pak.global_priority_by_format::jsonb IS DISTINCT FROM rebuilt.global_priority_by_format;
WITH rebuilt AS (
SELECT
pak.id,
jsonb_object_agg(
public.aether_canonical_api_format_alias(entry.key),
entry.value
ORDER BY entry.ordinality
) AS health_by_format
FROM public.provider_api_keys AS pak
CROSS JOIN LATERAL jsonb_each(
CASE
WHEN jsonb_typeof(pak.health_by_format) = 'object' THEN pak.health_by_format
ELSE '{}'::jsonb
END
) WITH ORDINALITY AS entry(key, value, ordinality)
WHERE pak.health_by_format IS NOT NULL
GROUP BY pak.id
)
UPDATE public.provider_api_keys AS pak
SET
health_by_format = rebuilt.health_by_format,
updated_at = NOW()
FROM rebuilt
WHERE pak.id = rebuilt.id
AND pak.health_by_format IS DISTINCT FROM rebuilt.health_by_format;
WITH rebuilt AS (
SELECT
pak.id,
jsonb_object_agg(
public.aether_canonical_api_format_alias(entry.key),
entry.value
ORDER BY entry.ordinality
) AS circuit_breaker_by_format
FROM public.provider_api_keys AS pak
CROSS JOIN LATERAL jsonb_each(
CASE
WHEN jsonb_typeof(pak.circuit_breaker_by_format) = 'object' THEN pak.circuit_breaker_by_format
ELSE '{}'::jsonb
END
) WITH ORDINALITY AS entry(key, value, ordinality)
WHERE pak.circuit_breaker_by_format IS NOT NULL
GROUP BY pak.id
)
UPDATE public.provider_api_keys AS pak
SET
circuit_breaker_by_format = rebuilt.circuit_breaker_by_format,
updated_at = NOW()
FROM rebuilt
WHERE pak.id = rebuilt.id
AND pak.circuit_breaker_by_format IS DISTINCT FROM rebuilt.circuit_breaker_by_format;
DROP FUNCTION public.aether_canonical_api_format_alias(text);

View File

@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260424000000;
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260428000000;
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
@@ -664,6 +664,7 @@ SELECT EXISTS (
20260422120000,
20260423000000,
20260424000000,
20260428000000,
]
);
}
@@ -816,6 +817,7 @@ SELECT EXISTS (
20260422120000,
20260423000000,
20260424000000,
20260428000000,
]
);
}

View File

@@ -913,7 +913,7 @@ mod tests {
None,
None,
Some(serde_json::json!(["anthropic"])),
Some(serde_json::json!(["claude:chat"])),
Some(serde_json::json!(["claude:messages"])),
Some(serde_json::json!(["claude-sonnet-4-5"])),
)
.expect("snapshot should build");
@@ -926,7 +926,7 @@ mod tests {
);
assert_eq!(
resolved.effective_allowed_api_formats(),
Some(&["claude:chat".to_string()][..])
Some(&["claude:messages".to_string()][..])
);
assert_eq!(
resolved.effective_allowed_models(),

View File

@@ -41,6 +41,7 @@ impl MinimalCandidateSelectionReadRepository for InMemoryMinimalCandidateSelecti
&& row.model_is_available
&& api_format_matches(&row.endpoint_api_format, api_format)
&& row.key_supports_api_format(api_format)
&& key_auth_channel_matches(row, api_format)
})
.cloned()
.collect::<Vec<_>>();
@@ -70,11 +71,42 @@ impl MinimalCandidateSelectionReadRepository for InMemoryMinimalCandidateSelecti
}
fn normalize_api_format(value: &str) -> String {
aether_ai_formats::normalize_legacy_openai_format_alias(value)
aether_ai_formats::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {
normalize_api_format(left) == normalize_api_format(right)
aether_ai_formats::api_format_alias_matches(left, right)
}
fn key_auth_channel_matches(row: &StoredMinimalCandidateSelectionRow, api_format: &str) -> bool {
let provider_type = row.provider_type.trim().to_ascii_lowercase();
let auth_type = row.key_auth_type.trim().to_ascii_lowercase();
let api_format = normalize_api_format(api_format);
match provider_type.as_str() {
"codex" => {
auth_type == "oauth"
&& matches!(
api_format.as_str(),
"openai:responses" | "openai:responses:compact" | "openai:image"
)
}
"claude_code" => auth_type == "oauth" && api_format == "claude:messages",
"kiro" => {
matches!(auth_type.as_str(), "oauth" | "bearer") && api_format == "claude:messages"
}
"gemini_cli" | "antigravity" => {
auth_type == "oauth" && api_format == "gemini:generate_content"
}
"vertex_ai" => {
(auth_type == "api_key" && api_format == "gemini:generate_content")
|| (matches!(auth_type.as_str(), "service_account" | "vertex_ai")
&& matches!(
api_format.as_str(),
"claude:messages" | "gemini:generate_content"
))
}
_ => auth_type != "oauth",
}
}
#[cfg(test)]

View File

@@ -65,18 +65,26 @@ WHERE p.is_active = TRUE
AND LOWER(pe.api_format) = LOWER($1)
AND (
pak.api_formats IS NULL
OR (
LOWER(BTRIM(p.provider_type)) IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity'
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)
WHERE LOWER(BTRIM(fmt.value)) = ANY($2::text[])
)
)
AND (
(
LOWER(BTRIM(p.provider_type)) = 'codex'
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($3) IN ('openai:responses', 'openai:responses:compact', 'openai:image')
)
OR (
LOWER(BTRIM(p.provider_type)) = 'claude_code'
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($3) = 'claude:messages'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'kiro'
AND LOWER($3) = 'claude:messages'
AND (
LOWER(BTRIM(pak.auth_type)) = 'oauth'
OR (
@@ -86,10 +94,34 @@ WHERE p.is_active = TRUE
)
)
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)
WHERE LOWER(fmt.value) = LOWER($1)
OR (
LOWER(BTRIM(p.provider_type)) IN ('gemini_cli', 'antigravity')
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($3) = 'gemini:generate_content'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'vertex_ai'
AND (
(
LOWER(BTRIM(pak.auth_type)) = 'api_key'
AND LOWER($3) = 'gemini:generate_content'
)
OR (
LOWER(BTRIM(pak.auth_type)) IN ('service_account', 'vertex_ai')
AND LOWER($3) IN ('claude:messages', 'gemini:generate_content')
)
)
)
OR (
LOWER(BTRIM(p.provider_type)) NOT IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity',
'kiro'
)
AND LOWER(BTRIM(pak.auth_type)) <> 'oauth'
)
)
ORDER BY
@@ -159,18 +191,26 @@ WHERE p.is_active = TRUE
AND gm.name = $2
AND (
pak.api_formats IS NULL
OR (
LOWER(BTRIM(p.provider_type)) IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity'
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)
WHERE LOWER(BTRIM(fmt.value)) = ANY($3::text[])
)
)
AND (
(
LOWER(BTRIM(p.provider_type)) = 'codex'
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($4) IN ('openai:responses', 'openai:responses:compact', 'openai:image')
)
OR (
LOWER(BTRIM(p.provider_type)) = 'claude_code'
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($4) = 'claude:messages'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'kiro'
AND LOWER($4) = 'claude:messages'
AND (
LOWER(BTRIM(pak.auth_type)) = 'oauth'
OR (
@@ -180,10 +220,34 @@ WHERE p.is_active = TRUE
)
)
)
OR EXISTS (
SELECT 1
FROM json_array_elements_text(pak.api_formats) AS fmt(value)
WHERE LOWER(fmt.value) = LOWER($1)
OR (
LOWER(BTRIM(p.provider_type)) IN ('gemini_cli', 'antigravity')
AND LOWER(BTRIM(pak.auth_type)) = 'oauth'
AND LOWER($4) = 'gemini:generate_content'
)
OR (
LOWER(BTRIM(p.provider_type)) = 'vertex_ai'
AND (
(
LOWER(BTRIM(pak.auth_type)) = 'api_key'
AND LOWER($4) = 'gemini:generate_content'
)
OR (
LOWER(BTRIM(pak.auth_type)) IN ('service_account', 'vertex_ai')
AND LOWER($4) IN ('claude:messages', 'gemini:generate_content')
)
)
)
OR (
LOWER(BTRIM(p.provider_type)) NOT IN (
'claude_code',
'codex',
'gemini_cli',
'vertex_ai',
'antigravity',
'kiro'
)
AND LOWER(BTRIM(pak.auth_type)) <> 'oauth'
)
)
ORDER BY
@@ -228,11 +292,16 @@ impl SqlxMinimalCandidateSelectionReadRepository {
api_format: &str,
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
let mut rows = Vec::new();
for api_format in api_format_aliases(api_format) {
let canonical_api_format = normalize_api_format(api_format);
let storage_aliases = api_format_aliases(&canonical_api_format);
let sql_match_aliases = sql_match_aliases(&storage_aliases);
for api_format in storage_aliases {
rows.extend(
Self::collect_query_rows(
sqlx::query(LIST_FOR_EXACT_API_FORMAT_SQL)
.bind(api_format)
.bind(sql_match_aliases.clone())
.bind(canonical_api_format.clone())
.fetch(&self.pool),
map_candidate_selection_row,
)
@@ -248,12 +317,17 @@ impl SqlxMinimalCandidateSelectionReadRepository {
global_model_name: &str,
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
let mut rows = Vec::new();
for api_format in api_format_aliases(api_format) {
let canonical_api_format = normalize_api_format(api_format);
let storage_aliases = api_format_aliases(&canonical_api_format);
let sql_match_aliases = sql_match_aliases(&storage_aliases);
for api_format in storage_aliases {
rows.extend(
Self::collect_query_rows(
sqlx::query(LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL)
.bind(api_format)
.bind(global_model_name)
.bind(sql_match_aliases.clone())
.bind(canonical_api_format.clone())
.fetch(&self.pool),
map_candidate_selection_row,
)
@@ -265,7 +339,18 @@ impl SqlxMinimalCandidateSelectionReadRepository {
}
fn api_format_aliases(api_format: &str) -> Vec<String> {
aether_ai_formats::openai_format_storage_aliases(api_format)
aether_ai_formats::api_format_storage_aliases(api_format)
}
fn normalize_api_format(api_format: &str) -> String {
aether_ai_formats::normalize_api_format_alias(api_format)
}
fn sql_match_aliases(api_formats: &[String]) -> Vec<String> {
api_formats
.iter()
.map(|value| value.trim().to_ascii_lowercase())
.collect()
}
fn dedupe_candidate_selection_rows(
@@ -521,7 +606,13 @@ fn parse_provider_model_mapping_object_lenient(
let api_formats = parse_string_list(
object.get("api_formats").cloned(),
"models.provider_model_mappings.api_formats",
)?;
)?
.map(|formats| {
formats
.into_iter()
.map(|value| aether_ai_formats::normalize_api_format_alias(&value))
.collect()
});
Ok(Some(StoredProviderModelMapping {
name: name.to_string(),

View File

@@ -12,8 +12,8 @@ const MODEL_FETCH_FORMAT_PRIORITY: &[&[&str]] = &[
"openai:responses",
"openai:responses:compact",
],
&["claude:chat", "claude:cli"],
&["gemini:chat", "gemini:cli"],
&["claude:messages"],
&["gemini:generate_content"],
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -221,10 +221,8 @@ pub fn endpoint_supports_rust_models_fetch(api_format: &str) -> bool {
"openai:chat"
| "openai:responses"
| "openai:responses:compact"
| "claude:chat"
| "claude:cli"
| "gemini:chat"
| "gemini:cli"
| "claude:messages"
| "gemini:generate_content"
)
}
@@ -239,25 +237,25 @@ pub fn provider_type_uses_preset_models(provider_type: &str) -> bool {
pub fn preset_models_for_provider(provider_type: &str) -> Option<Vec<Value>> {
let models = match provider_type.trim().to_ascii_lowercase().as_str() {
"gemini_cli" => vec![
preset_model("gemini-2.5-pro", "google", "Gemini 2.5 Pro", "gemini:cli"),
preset_model("gemini-2.5-flash", "google", "Gemini 2.5 Flash", "gemini:cli"),
preset_model("gemini-3-pro-preview", "google", "Gemini 3 Pro Preview", "gemini:cli"),
preset_model("gemini-3-flash-preview", "google", "Gemini 3 Flash Preview", "gemini:cli"),
preset_model("gemini-3.1-pro-preview", "google", "Gemini 3.1 Pro Preview", "gemini:cli"),
preset_model("gemini-2.5-pro", "google", "Gemini 2.5 Pro", "gemini:generate_content"),
preset_model("gemini-2.5-flash", "google", "Gemini 2.5 Flash", "gemini:generate_content"),
preset_model("gemini-3-pro-preview", "google", "Gemini 3 Pro Preview", "gemini:generate_content"),
preset_model("gemini-3-flash-preview", "google", "Gemini 3 Flash Preview", "gemini:generate_content"),
preset_model("gemini-3.1-pro-preview", "google", "Gemini 3.1 Pro Preview", "gemini:generate_content"),
],
"kiro" => vec![
preset_model("claude-sonnet-4.5", "anthropic", "Claude Sonnet 4.5", "claude:cli"),
preset_model("claude-sonnet-4.6", "anthropic", "Claude Sonnet 4.6", "claude:cli"),
preset_model("claude-opus-4.5", "anthropic", "Claude Opus 4.5", "claude:cli"),
preset_model("claude-opus-4.6", "anthropic", "Claude Opus 4.6", "claude:cli"),
preset_model("claude-haiku-4.5", "anthropic", "Claude Haiku 4.5", "claude:cli"),
preset_model("claude-sonnet-4.5", "anthropic", "Claude Sonnet 4.5", "claude:messages"),
preset_model("claude-sonnet-4.6", "anthropic", "Claude Sonnet 4.6", "claude:messages"),
preset_model("claude-opus-4.5", "anthropic", "Claude Opus 4.5", "claude:messages"),
preset_model("claude-opus-4.6", "anthropic", "Claude Opus 4.6", "claude:messages"),
preset_model("claude-haiku-4.5", "anthropic", "Claude Haiku 4.5", "claude:messages"),
],
"claude_code" => vec![
preset_model("claude-opus-4-5-20251101", "anthropic", "Claude Opus 4.5", "claude:cli"),
preset_model("claude-opus-4-6", "anthropic", "Claude Opus 4.6", "claude:cli"),
preset_model("claude-sonnet-4-6", "anthropic", "Claude Sonnet 4.6", "claude:cli"),
preset_model("claude-sonnet-4-5-20250929", "anthropic", "Claude Sonnet 4.5", "claude:cli"),
preset_model("claude-haiku-4-5-20251001", "anthropic", "Claude Haiku 4.5", "claude:cli"),
preset_model("claude-opus-4-5-20251101", "anthropic", "Claude Opus 4.5", "claude:messages"),
preset_model("claude-opus-4-6", "anthropic", "Claude Opus 4.6", "claude:messages"),
preset_model("claude-sonnet-4-6", "anthropic", "Claude Sonnet 4.6", "claude:messages"),
preset_model("claude-sonnet-4-5-20250929", "anthropic", "Claude Sonnet 4.5", "claude:messages"),
preset_model("claude-haiku-4-5-20251001", "anthropic", "Claude Haiku 4.5", "claude:messages"),
],
"codex" => vec![
preset_model("gpt-5", "openai", "GPT-5", "openai:responses"),
@@ -570,7 +568,7 @@ fn wildcard_matches(pattern: &str, model_id: &str) -> bool {
}
fn normalize_api_format(value: &str) -> String {
aether_ai_formats::normalize_legacy_openai_format_alias(value)
aether_ai_formats::normalize_api_format_alias(value)
}
#[cfg(test)]
@@ -726,7 +724,7 @@ mod tests {
#[test]
fn parse_models_response_page_reads_claude_pagination_state() {
let parsed = parse_models_response_page(
"claude:chat",
"claude:messages",
&json!({
"data": [{"id": "claude-sonnet-4"}],
"has_more": true,
@@ -750,8 +748,8 @@ mod tests {
),
sample_endpoint(
"provider-1",
"endpoint-cli",
"openai:cli",
"endpoint-compact",
"openai:responses:compact",
"https://example.com",
),
sample_endpoint(
@@ -769,8 +767,8 @@ mod tests {
let endpoints = vec![
sample_endpoint(
"provider-1",
"endpoint-cli",
"openai:cli",
"endpoint-compact",
"openai:responses:compact",
"https://example.com",
),
sample_endpoint(

View File

@@ -291,7 +291,7 @@ async fn fetch_vertex_api_key_models(
&url,
auth_config,
"google",
"gemini:chat",
"gemini:generate_content",
None,
)
.await?;
@@ -362,8 +362,8 @@ async fn fetch_vertex_service_account_models(
format!("https://{region}-aiplatform.googleapis.com")
};
for (publisher, transport, api_format) in [
("google", gemini_transport, "gemini:chat"),
("anthropic", claude_transport, "claude:chat"),
("google", gemini_transport, "gemini:generate_content"),
("anthropic", claude_transport, "claude:messages"),
] {
let url =
build_vertex_service_account_list_url(&base, &project_id, &region, publisher, None);
@@ -517,7 +517,7 @@ async fn exchange_vertex_service_account_token(
body_ref: None,
},
stream: false,
client_api_format: "gemini:chat".to_string(),
client_api_format: "gemini:generate_content".to_string(),
provider_api_format: "vertex_ai:service_account_token".to_string(),
model_name: Some("token".to_string()),
proxy: runtime.resolve_model_fetch_proxy(transport).await,
@@ -618,7 +618,7 @@ fn parse_antigravity_models_response(body: &Value) -> Result<(Vec<Value>, Option
"object": "model",
"owned_by": "antigravity",
"display_name": display_name,
"api_formats": ["gemini:chat"],
"api_formats": ["gemini:generate_content"],
}));
let quota_payload = build_antigravity_quota_payload(model_object.get("quotaInfo"));
@@ -844,7 +844,9 @@ fn vertex_effective_format(model_id: &str, auth_config: Option<&Value>) -> Strin
&& model_id.starts_with(prefix)
&& api_format.as_str().is_some()
{
return normalize_api_format(api_format.as_str().unwrap_or("gemini:chat"));
return normalize_api_format(
api_format.as_str().unwrap_or("gemini:generate_content"),
);
}
}
}
@@ -856,9 +858,9 @@ fn vertex_effective_format(model_id: &str, auth_config: Option<&Value>) -> Strin
}
}
if model_id.starts_with("claude-") {
"claude:chat".to_string()
"claude:messages".to_string()
} else {
"gemini:chat".to_string()
"gemini:generate_content".to_string()
}
}
@@ -1150,9 +1152,9 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "gemini:cli".to_string(),
api_format: "gemini:generate_content".to_string(),
api_family: Some("gemini".to_string()),
endpoint_kind: Some("cli".to_string()),
endpoint_kind: Some("generate_content".to_string()),
is_active: true,
base_url: "https://aiplatform.googleapis.com".to_string(),
header_rules: None,
@@ -1169,7 +1171,7 @@ mod tests {
name: "key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:cli".to_string()]),
api_formats: Some(vec!["gemini:generate_content".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -1204,7 +1206,7 @@ mod tests {
assert_eq!(outcome.cached_models.len(), 1);
assert_eq!(
outcome.cached_models[0]["api_formats"][0].as_str(),
Some("gemini:chat")
Some("gemini:generate_content")
);
}
}

View File

@@ -85,7 +85,7 @@ pub async fn build_standard_models_fetch_execution_plan(
) -> Result<ExecutionPlan, String> {
let api_format = transport.endpoint.api_format.trim().to_ascii_lowercase();
let provider_api_format = api_format.clone();
let mut headers = standard_models_fetch_headers(&api_format);
let mut headers = standard_models_fetch_headers(&api_format, &transport.provider.provider_type);
let mut protected_headers = Vec::<String>::new();
if api_format.starts_with("openai:") || api_format.starts_with("claude:") {
@@ -167,7 +167,7 @@ pub async fn build_antigravity_fetch_available_models_plan(
headers,
content_type: Some("application/json".to_string()),
body: RequestBody::from_json(json!({ "project": project_id })),
client_api_format: "gemini:chat".to_string(),
client_api_format: "gemini:generate_content".to_string(),
provider_api_format: ANTIGRAVITY_FETCH_PROVIDER_API_FORMAT.to_string(),
model_name: Some("fetchAvailableModels".to_string()),
},
@@ -207,7 +207,7 @@ pub async fn build_gemini_cli_load_code_assist_plan(
"pluginType": "GEMINI",
}
})),
client_api_format: "gemini:cli".to_string(),
client_api_format: "gemini:generate_content".to_string(),
provider_api_format: GEMINI_CLI_LOAD_CODE_ASSIST_PROVIDER_API_FORMAT.to_string(),
model_name: Some("loadCodeAssist".to_string()),
},
@@ -222,7 +222,7 @@ pub async fn build_vertex_models_fetch_execution_plan(
api_format: &str,
auth_header: Option<(String, String)>,
) -> Result<ExecutionPlan, String> {
let mut headers = standard_models_fetch_headers(api_format);
let mut headers = standard_models_fetch_headers(api_format, &transport.provider.provider_type);
let mut protected_headers = Vec::<String>::new();
if let Some((name, value)) = auth_header {
protected_headers.push(name.clone());
@@ -380,34 +380,35 @@ fn apply_fetch_header_rules(
Ok(headers)
}
fn standard_models_fetch_headers(api_format: &str) -> BTreeMap<String, String> {
let api_format = aether_ai_formats::normalize_legacy_openai_format_alias(api_format);
fn standard_models_fetch_headers(
api_format: &str,
provider_type: &str,
) -> BTreeMap<String, String> {
let api_format = aether_ai_formats::normalize_api_format_alias(api_format);
let provider_type = provider_type.trim().to_ascii_lowercase();
match api_format.as_str() {
"openai:responses" | "openai:responses:compact" => BTreeMap::from([(
"user-agent".to_string(),
OPENAI_RESPONSES_USER_AGENT.to_string(),
)]),
"claude:chat" => BTreeMap::from([(
"anthropic-version".to_string(),
CLAUDE_VERSION_HEADER.to_string(),
)]),
"claude:cli" => BTreeMap::from([
("user-agent".to_string(), CLAUDE_CLI_USER_AGENT.to_string()),
(
"claude:messages" => {
let mut headers = BTreeMap::from([(
"anthropic-version".to_string(),
CLAUDE_VERSION_HEADER.to_string(),
),
]),
"gemini:chat" => BROWSER_FINGERPRINT_HEADERS
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect(),
"gemini:cli" => {
)]);
if matches!(provider_type.as_str(), "claude_code" | "kiro") {
headers.insert("user-agent".to_string(), CLAUDE_CLI_USER_AGENT.to_string());
}
headers
}
"gemini:generate_content" => {
let mut headers = BROWSER_FINGERPRINT_HEADERS
.iter()
.map(|(key, value)| (key.to_string(), value.to_string()))
.collect::<BTreeMap<_, _>>();
headers.insert("user-agent".to_string(), GEMINI_CLI_USER_AGENT.to_string());
if provider_type == "gemini_cli" {
headers.insert("user-agent".to_string(), GEMINI_CLI_USER_AGENT.to_string());
}
headers
}
_ => BTreeMap::new(),
@@ -625,7 +626,7 @@ mod tests {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("custom", "claude:chat", "api_key");
let mut transport = sample_transport("custom", "claude:messages", "api_key");
transport.key.decrypted_auth_config = None;
let plan =
build_standard_models_fetch_execution_plan(&runtime, &transport, Some("cursor-1"))
@@ -652,7 +653,7 @@ mod tests {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("custom", "gemini:chat", "api_key");
let mut transport = sample_transport("custom", "gemini:generate_content", "api_key");
transport.key.decrypted_auth_config = None;
let plan = build_models_fetch_execution_plan(&runtime, &transport)
.await
@@ -677,7 +678,7 @@ mod tests {
),
proxy: None,
};
let transport = sample_transport("antigravity", "gemini:chat", "oauth");
let transport = sample_transport("antigravity", "gemini:generate_content", "oauth");
let plan = build_antigravity_fetch_available_models_plan(
&runtime,
&transport,
@@ -716,7 +717,7 @@ mod tests {
),
proxy: None,
};
let transport = sample_transport("gemini_cli", "gemini:cli", "oauth");
let transport = sample_transport("gemini_cli", "gemini:generate_content", "oauth");
let plan = build_gemini_cli_load_code_assist_plan(&runtime, &transport)
.await
.expect("plan");
@@ -738,13 +739,13 @@ mod tests {
oauth_auth: None,
proxy: None,
};
let mut transport = sample_transport("vertex_ai", "claude:chat", "api_key");
let mut transport = sample_transport("vertex_ai", "claude:messages", "api_key");
transport.key.decrypted_auth_config = None;
let plan = build_vertex_models_fetch_execution_plan(
&runtime,
&transport,
"https://aiplatform.googleapis.com/v1/publishers/google/models?key=secret",
"gemini:chat",
"gemini:generate_content",
None,
)
.await

View File

@@ -56,10 +56,9 @@ pub fn classify_local_antigravity_request_support(
);
}
let endpoint_format = transport.endpoint.api_format.trim();
if !endpoint_format.eq_ignore_ascii_case("gemini:chat")
&& !endpoint_format.eq_ignore_ascii_case("gemini:cli")
{
let endpoint_format =
aether_ai_formats::normalize_api_format_alias(&transport.endpoint.api_format);
if endpoint_format != "gemini:generate_content" {
return AntigravityRequestSideSupport::Unsupported(
AntigravityRequestSideUnsupportedReason::UnsupportedApiFormat,
);

View File

@@ -302,7 +302,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:chat".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("chat".to_string()),
is_active: true,

View File

@@ -99,7 +99,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
@@ -118,7 +118,7 @@ mod tests {
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -136,7 +136,7 @@ mod tests {
fn supports_claude_code_transport_when_auth_and_tls_are_valid() {
assert!(supports_local_claude_code_transport_with_network(
&sample_transport(),
"claude:cli"
"claude:messages"
));
}
@@ -147,7 +147,10 @@ mod tests {
transport.key.decrypted_api_key = "__placeholder__".to_string();
assert_eq!(
local_claude_code_transport_unsupported_reason_with_network(&transport, "claude:cli"),
local_claude_code_transport_unsupported_reason_with_network(
&transport,
"claude:messages"
),
Some("transport_auth_unavailable")
);
}

View File

@@ -176,7 +176,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
@@ -195,7 +195,7 @@ mod tests {
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -25,12 +25,10 @@ pub fn local_kiro_request_transport_unsupported_reason_with_network(
{
return Some("transport_provider_type_unsupported");
}
if !transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case("claude:cli")
{
if !aether_ai_formats::api_format_alias_matches(
&transport.endpoint.api_format,
"claude:messages",
) {
return Some("transport_api_format_mismatch");
}
if !header_rules_are_locally_supported(transport.endpoint.header_rules.as_ref()) {
@@ -69,12 +67,10 @@ pub fn supports_local_kiro_request_transport(transport: &GatewayProviderTranspor
{
return false;
}
if !transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case("claude:cli")
{
if !aether_ai_formats::api_format_alias_matches(
&transport.endpoint.api_format,
"claude:messages",
) {
return false;
}
supports_local_kiro_request_shape(
@@ -120,9 +116,9 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
endpoint_kind: Some("messages".to_string()),
is_active: true,
base_url: "https://kiro.example".to_string(),
header_rules: None,
@@ -139,7 +135,7 @@ mod tests {
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -212,7 +212,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
@@ -231,7 +231,7 @@ mod tests {
name: "key".to_string(),
auth_type: "bearer".to_string(),
is_active: true,
api_formats: Some(vec!["claude:cli".to_string()]),
api_formats: Some(vec!["claude:messages".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,

View File

@@ -651,7 +651,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "claude:cli".to_string(),
api_format: "claude:messages".to_string(),
api_family: Some("claude".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,

View File

@@ -200,5 +200,5 @@ fn local_same_format_transport_unsupported_reason(
}
fn same_api_format(left: &str, right: &str) -> bool {
aether_ai_formats::legacy_openai_format_alias_matches(left, right)
aether_ai_formats::api_format_alias_matches(left, right)
}

View File

@@ -62,8 +62,8 @@ const CLAUDE_CODE_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProvider
version: 1,
base_url: "https://api.anthropic.com",
endpoints: &[FixedProviderEndpointTemplate {
item_key: "claude:cli",
api_format: "claude:cli",
item_key: "claude:messages",
api_format: "claude:messages",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
}],
@@ -100,8 +100,8 @@ const KIRO_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderTemplat
version: 1,
base_url: "https://q.{region}.amazonaws.com",
endpoints: &[FixedProviderEndpointTemplate {
item_key: "claude:cli",
api_format: "claude:cli",
item_key: "claude:messages",
api_format: "claude:messages",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
}],
@@ -112,8 +112,8 @@ const GEMINI_CLI_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderT
version: 1,
base_url: "https://cloudcode-pa.googleapis.com",
endpoints: &[FixedProviderEndpointTemplate {
item_key: "gemini:cli",
api_format: "gemini:cli",
item_key: "gemini:generate_content",
api_format: "gemini:generate_content",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
}],
@@ -125,14 +125,14 @@ const VERTEX_AI_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProviderTe
base_url: "https://aiplatform.googleapis.com",
endpoints: &[
FixedProviderEndpointTemplate {
item_key: "gemini:chat",
api_format: "gemini:chat",
item_key: "gemini:generate_content",
api_format: "gemini:generate_content",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
FixedProviderEndpointTemplate {
item_key: "claude:chat",
api_format: "claude:chat",
item_key: "claude:messages",
api_format: "claude:messages",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
},
@@ -144,8 +144,8 @@ const ANTIGRAVITY_FIXED_PROVIDER_TEMPLATE: FixedProviderTemplate = FixedProvider
version: 1,
base_url: "https://cloudcode-pa.googleapis.com",
endpoints: &[FixedProviderEndpointTemplate {
item_key: "gemini:chat",
api_format: "gemini:chat",
item_key: "gemini:generate_content",
api_format: "gemini:generate_content",
custom_path: None,
config_defaults: EMPTY_ENDPOINT_CONFIG_DEFAULTS,
}],
@@ -181,7 +181,7 @@ pub fn fixed_provider_endpoint_template_by_api_format(
provider_type: &str,
api_format: &str,
) -> Option<&'static FixedProviderEndpointTemplate> {
let normalized = aether_ai_formats::normalize_legacy_openai_format_alias(api_format);
let normalized = aether_ai_formats::normalize_api_format_alias(api_format);
fixed_provider_template(provider_type)?
.endpoints
.iter()

View File

@@ -60,9 +60,7 @@ pub fn build_transport_request_url(
));
}
let url = match aether_ai_formats::normalize_legacy_openai_format_alias(&provider_api_format)
.as_str()
{
let url = match aether_ai_formats::normalize_api_format_alias(&provider_api_format).as_str() {
"openai:chat" => Some(build_openai_chat_url(
&transport.endpoint.base_url,
params.request_query,
@@ -77,11 +75,11 @@ pub fn build_transport_request_url(
params.request_query,
true,
)),
"claude:chat" | "claude:cli" => Some(build_claude_messages_url(
"claude:messages" => Some(build_claude_messages_url(
&transport.endpoint.base_url,
params.request_query,
)),
"gemini:chat" | "gemini:cli" => build_gemini_content_url(
"gemini:generate_content" => build_gemini_content_url(
&transport.endpoint.base_url,
params.mapped_model?,
params.upstream_is_stream,
@@ -325,7 +323,7 @@ mod tests {
fn uses_vertex_hook_before_custom_path_for_custom_aiplatform_transport() {
let transport = sample_transport(
"custom",
"gemini:cli",
"gemini:generate_content",
"https://aiplatform.googleapis.com",
Some("/custom/{model}:{action}"),
);
@@ -333,7 +331,7 @@ mod tests {
let url = build_transport_request_url(
&transport,
TransportRequestUrlParams {
provider_api_format: "gemini:cli",
provider_api_format: "gemini:generate_content",
mapped_model: Some("gemini-3.1-pro-preview"),
upstream_is_stream: true,
request_query: Some("foo=bar"),
@@ -376,7 +374,7 @@ mod tests {
fn expands_custom_path_templates_when_hook_does_not_apply() {
let transport = sample_transport(
"custom",
"gemini:chat",
"gemini:generate_content",
"https://generativelanguage.googleapis.com",
Some("/v1beta/models/{model}:{action}"),
);
@@ -384,7 +382,7 @@ mod tests {
let url = build_transport_request_url(
&transport,
TransportRequestUrlParams {
provider_api_format: "gemini:chat",
provider_api_format: "gemini:generate_content",
mapped_model: Some("gemini-2.5-pro"),
upstream_is_stream: false,
request_query: Some("key=client-key&foo=bar"),
@@ -403,7 +401,7 @@ mod tests {
fn keeps_original_custom_path_when_template_params_are_missing() {
let transport = sample_transport(
"custom",
"claude:chat",
"claude:messages",
"https://api.example.com",
Some("/v1/messages/{model}"),
);
@@ -411,7 +409,7 @@ mod tests {
let url = build_transport_request_url(
&transport,
TransportRequestUrlParams {
provider_api_format: "claude:chat",
provider_api_format: "claude:messages",
mapped_model: None,
upstream_is_stream: false,
request_query: None,

View File

@@ -750,7 +750,7 @@ mod tests {
let endpoint = StoredProviderCatalogEndpoint::new(
"endpoint-safe-3".to_string(),
"provider-1".to_string(),
"gemini:chat".to_string(),
"gemini:generate_content".to_string(),
Some("gemini".to_string()),
Some("chat".to_string()),
true,
@@ -785,7 +785,7 @@ mod tests {
)
.expect("key should build")
.with_transport_fields(
Some(serde_json::json!(["gemini:chat"])),
Some(serde_json::json!(["gemini:generate_content"])),
encrypted_api_key,
Some(encrypted_auth_config),
None,

View File

@@ -68,7 +68,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "gemini:chat".to_string(),
api_format: "gemini:generate_content".to_string(),
api_family: Some("gemini".to_string()),
endpoint_kind: Some("chat".to_string()),
is_active: true,
@@ -87,7 +87,7 @@ mod tests {
name: "key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:chat".to_string()]),
api_formats: Some(vec!["gemini:generate_content".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -127,7 +127,7 @@ mod tests {
fn resolves_query_auth_for_custom_aiplatform_transport() {
let mut transport = sample_transport();
transport.provider.provider_type = "custom".to_string();
transport.endpoint.api_format = "gemini:cli".to_string();
transport.endpoint.api_format = "gemini:generate_content".to_string();
let auth = resolve_local_vertex_api_key_query_auth(&transport)
.expect("custom aiplatform transport should resolve");

View File

@@ -97,7 +97,7 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "gemini:cli".to_string(),
api_format: "gemini:generate_content".to_string(),
api_family: Some("gemini".to_string()),
endpoint_kind: Some("cli".to_string()),
is_active: true,
@@ -116,7 +116,7 @@ mod tests {
name: "key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:cli".to_string()]),
api_formats: Some(vec!["gemini:generate_content".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -156,8 +156,13 @@ mod tests {
#[test]
fn detects_vertex_query_auth_usage_for_gemini_formats() {
let transport = sample_transport();
assert!(uses_vertex_api_key_query_auth(&transport, "gemini:cli"));
assert!(uses_vertex_api_key_query_auth(&transport, "gemini:chat"));
assert!(!uses_vertex_api_key_query_auth(&transport, "claude:chat"));
assert!(uses_vertex_api_key_query_auth(
&transport,
"gemini:generate_content"
));
assert!(!uses_vertex_api_key_query_auth(
&transport,
"claude:messages"
));
}
}

View File

@@ -26,16 +26,8 @@ pub fn local_vertex_api_key_gemini_transport_unsupported_reason_with_network(
Some("key_inactive")
};
}
if !transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case("gemini:chat")
&& !transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case("gemini:cli")
if aether_ai_formats::normalize_api_format_alias(&transport.endpoint.api_format)
!= "gemini:generate_content"
{
return Some("transport_api_format_mismatch");
}
@@ -66,7 +58,7 @@ pub fn supports_local_vertex_api_key_gemini_transport(
) -> bool {
supports_local_vertex_api_key_same_format_transport(
transport,
&["gemini:chat", "gemini:cli"],
&["gemini:generate_content"],
false,
)
}
@@ -80,13 +72,21 @@ pub fn supports_local_vertex_api_key_gemini_transport_with_network(
pub fn supports_local_vertex_api_key_imagen_transport(
transport: &GatewayProviderTransportSnapshot,
) -> bool {
supports_local_vertex_api_key_same_format_transport(transport, &["gemini:chat"], false)
supports_local_vertex_api_key_same_format_transport(
transport,
&["gemini:generate_content"],
false,
)
}
pub fn supports_local_vertex_api_key_imagen_transport_with_network(
transport: &GatewayProviderTransportSnapshot,
) -> bool {
supports_local_vertex_api_key_same_format_transport(transport, &["gemini:chat"], true)
supports_local_vertex_api_key_same_format_transport(
transport,
&["gemini:generate_content"],
true,
)
}
fn supports_local_vertex_api_key_same_format_transport(
@@ -97,18 +97,11 @@ fn supports_local_vertex_api_key_same_format_transport(
if !transport.provider.is_active || !transport.endpoint.is_active || !transport.key.is_active {
return false;
}
if !transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case(api_formats[0])
&& !api_formats.iter().any(|api_format| {
transport
.endpoint
.api_format
.trim()
.eq_ignore_ascii_case(api_format)
})
let endpoint_api_format =
aether_ai_formats::normalize_api_format_alias(&transport.endpoint.api_format);
if !api_formats
.iter()
.any(|api_format| endpoint_api_format.eq_ignore_ascii_case(api_format))
{
return false;
}
@@ -190,9 +183,9 @@ mod tests {
endpoint: GatewayProviderTransportEndpoint {
id: "endpoint-1".to_string(),
provider_id: "provider-1".to_string(),
api_format: "gemini:chat".to_string(),
api_format: "gemini:generate_content".to_string(),
api_family: Some("gemini".to_string()),
endpoint_kind: Some("chat".to_string()),
endpoint_kind: Some("generate_content".to_string()),
is_active: true,
base_url: "https://aiplatform.googleapis.com".to_string(),
header_rules: None,
@@ -209,7 +202,7 @@ mod tests {
name: "key".to_string(),
auth_type: "api_key".to_string(),
is_active: true,
api_formats: Some(vec!["gemini:chat".to_string()]),
api_formats: Some(vec!["gemini:generate_content".to_string()]),
allowed_models: None,
capabilities: None,
rate_multipliers: None,
@@ -231,17 +224,17 @@ mod tests {
}
#[test]
fn supports_vertex_api_key_gemini_cli_subset() {
fn supports_vertex_api_key_gemini_generate_content_subset() {
let mut transport = sample_transport();
transport.endpoint.api_format = "gemini:cli".to_string();
transport.endpoint.api_format = "gemini:generate_content".to_string();
assert!(supports_local_vertex_api_key_gemini_transport(&transport));
}
#[test]
fn supports_custom_aiplatform_gemini_cli_subset() {
fn supports_custom_aiplatform_gemini_generate_content_subset() {
let mut transport = sample_transport();
transport.provider.provider_type = "custom".to_string();
transport.endpoint.api_format = "gemini:cli".to_string();
transport.endpoint.api_format = "gemini:generate_content".to_string();
assert!(supports_local_vertex_api_key_gemini_transport(&transport));
}

View File

@@ -1090,14 +1090,14 @@ mod tests {
Some(serde_json::json!({
"openai:chat": {"health_score": 0.85},
"openai:responses": {"health_score": 0.45},
"claude:chat": {"health_score": 0.70}
"claude:messages": {"health_score": 0.70}
})),
None,
);
assert_eq!(aggregate_provider_key_health_score(&key), Some(0.45));
assert_eq!(
effective_provider_key_health_score(&key, "gemini:chat"),
effective_provider_key_health_score(&key, "gemini:generate_content"),
Some(0.45)
);
}

View File

@@ -279,7 +279,7 @@ pub fn extract_global_priority_for_format(
}
pub fn normalize_api_format(value: &str) -> String {
aether_ai_formats::normalize_legacy_openai_format_alias(value)
aether_ai_formats::normalize_api_format_alias(value)
}
fn row_has_candidate_model_name(

View File

@@ -103,16 +103,16 @@ pub fn infer_internal_finalize_signature(payload: &GatewaySyncReportRequest) ->
return Some("openai:video".to_string());
}
if report_kind.starts_with("claude_chat_") {
return Some("claude:chat".to_string());
return Some("claude:messages".to_string());
}
if report_kind.starts_with("claude_cli_") {
return Some("claude:cli".to_string());
return Some("claude:messages".to_string());
}
if report_kind.starts_with("gemini_chat_") {
return Some("gemini:chat".to_string());
return Some("gemini:generate_content".to_string());
}
if report_kind.starts_with("gemini_cli_") {
return Some("gemini:cli".to_string());
return Some("gemini:generate_content".to_string());
}
if report_kind.starts_with("gemini_video_") {
return Some("gemini:video".to_string());
@@ -121,7 +121,7 @@ pub fn infer_internal_finalize_signature(payload: &GatewaySyncReportRequest) ->
}
pub fn resolve_internal_finalize_route(signature: &str) -> Option<InternalFinalizeRoute> {
match aether_ai_formats::normalize_legacy_openai_format_alias(signature).as_str() {
match aether_ai_formats::normalize_api_format_alias(signature).as_str() {
"openai:chat" => Some(InternalFinalizeRoute {
public_path: "/v1/chat/completions",
route_family: "openai",
@@ -147,25 +147,15 @@ pub fn resolve_internal_finalize_route(signature: &str) -> Option<InternalFinali
route_family: "openai",
route_kind: "video",
}),
"claude:chat" => Some(InternalFinalizeRoute {
"claude:messages" => Some(InternalFinalizeRoute {
public_path: "/v1/messages",
route_family: "claude",
route_kind: "chat",
route_kind: "messages",
}),
"claude:cli" => Some(InternalFinalizeRoute {
public_path: "/v1/messages",
route_family: "claude",
route_kind: "cli",
}),
"gemini:chat" => Some(InternalFinalizeRoute {
"gemini:generate_content" => Some(InternalFinalizeRoute {
public_path: "/v1beta/models",
route_family: "gemini",
route_kind: "chat",
}),
"gemini:cli" => Some(InternalFinalizeRoute {
public_path: "/v1beta/models",
route_family: "gemini",
route_kind: "cli",
route_kind: "generate_content",
}),
"gemini:video" => Some(InternalFinalizeRoute {
public_path: "/v1beta/models",
@@ -525,10 +515,7 @@ mod tests {
route_kind: "responses:compact",
})
);
assert_eq!(
resolve_internal_finalize_route("openai:compact"),
resolve_internal_finalize_route("openai:responses:compact")
);
assert_eq!(resolve_internal_finalize_route("openai:compact"), None);
assert_eq!(
resolve_internal_finalize_route("gemini:video"),
Some(InternalFinalizeRoute {

View File

@@ -331,7 +331,7 @@ mod tests {
"cache_read_input_tokens": 10
}
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 0);
@@ -411,7 +411,7 @@ mod tests {
"cache_creation_input_tokens": 4,
"cache_read_input_tokens": 1
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 10);
@@ -447,7 +447,7 @@ mod tests {
}
]
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 5);
@@ -474,7 +474,7 @@ mod tests {
}
]
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 5);
@@ -492,7 +492,7 @@ mod tests {
"cache_read_input_tokens": 52873,
"output_tokens": 973
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 4941);
@@ -520,7 +520,7 @@ mod tests {
},
"service_tier": "standard"
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 2095);
@@ -544,7 +544,7 @@ mod tests {
"cache_creation_input_tokens": 5191,
"cache_read_input_tokens": 97634
}),
"claude:chat",
"claude:messages",
);
assert_eq!(usage.input_tokens, 1);
@@ -565,7 +565,7 @@ mod tests {
"cachedContentTokenCount": 2
}
}),
"gemini:chat",
"gemini:generate_content",
);
assert_eq!(usage.input_tokens, 14);
@@ -596,7 +596,7 @@ mod tests {
}
]
}),
"gemini:chat",
"gemini:generate_content",
);
assert_eq!(usage.input_tokens, 14);

View File

@@ -2453,7 +2453,7 @@ mod tests {
"messages": [{"role": "user", "content": "hello"}]
})),
stream: false,
client_api_format: "claude:cli".to_string(),
client_api_format: "claude:messages".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
@@ -2561,7 +2561,7 @@ mod tests {
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5.4"})),
stream: true,
client_api_format: "claude:cli".to_string(),
client_api_format: "claude:messages".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
@@ -2618,7 +2618,7 @@ mod tests {
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5.4"})),
stream: false,
client_api_format: "claude:cli".to_string(),
client_api_format: "claude:messages".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
@@ -2629,7 +2629,7 @@ mod tests {
trace_id: "trace-sync-usage-deep-1".to_string(),
report_kind: "claude_cli_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "claude:cli",
"client_api_format": "claude:messages",
"provider_api_format": "openai:responses",
"needs_conversion": true,
"original_request_body": nested,
@@ -3078,7 +3078,7 @@ mod tests {
},
stream: false,
client_api_format: "openai:chat".to_string(),
provider_api_format: "gemini:chat".to_string(),
provider_api_format: "gemini:generate_content".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
@@ -3089,7 +3089,7 @@ mod tests {
report_kind: "openai_chat_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"needs_conversion": true
})),
status_code: 200,
@@ -3293,7 +3293,7 @@ mod tests {
},
stream: false,
client_api_format: "openai:chat".to_string(),
provider_api_format: "gemini:chat".to_string(),
provider_api_format: "gemini:generate_content".to_string(),
model_name: Some("gpt-5".to_string()),
proxy: None,
tls_profile: None,
@@ -3304,7 +3304,7 @@ mod tests {
report_kind: "openai_chat_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "openai:chat",
"provider_api_format": "gemini:chat",
"provider_api_format": "gemini:generate_content",
"needs_conversion": true
})),
status_code: 200,
@@ -3579,7 +3579,7 @@ mod tests {
"input": [{"role": "user", "content": "provider-side compiled body"}],
})),
stream: false,
client_api_format: "claude:cli".to_string(),
client_api_format: "claude:messages".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
@@ -3590,7 +3590,7 @@ mod tests {
trace_id: "trace-sync-no-client-echo-1".to_string(),
report_kind: "claude_cli_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "claude:cli",
"client_api_format": "claude:messages",
"provider_api_format": "openai:responses",
"needs_conversion": true,
"original_request_body": null,
@@ -3633,7 +3633,7 @@ mod tests {
content_encoding: None,
body: RequestBody::from_json(json!({"model": "gpt-5.4"})),
stream: false,
client_api_format: "claude:cli".to_string(),
client_api_format: "claude:messages".to_string(),
provider_api_format: "openai:responses".to_string(),
model_name: Some("gpt-5.4".to_string()),
proxy: None,
@@ -3644,7 +3644,7 @@ mod tests {
trace_id: "trace-sync-null-error-1".to_string(),
report_kind: "claude_cli_sync_success".to_string(),
report_context: Some(json!({
"client_api_format": "claude:cli",
"client_api_format": "claude:messages",
"provider_api_format": "openai:responses",
"needs_conversion": true,
})),