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

@@ -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));
}