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

@@ -185,11 +185,8 @@ fn select_primary_credential(
if signature.starts_with("gemini:") {
return select_gemini_credential(bundle);
}
if signature == "claude:cli" {
return select_claude_cli_credential(bundle);
}
if signature.starts_with("claude:") {
return select_claude_chat_credential(bundle);
return select_claude_messages_credential(bundle);
}
if signature.starts_with("openai:") {
return select_openai_credential(bundle);
@@ -212,29 +209,15 @@ fn select_openai_credential(bundle: &GatewayCredentialBundle) -> Option<GatewayP
.or_else(|| select_cookie_credential(bundle))
}
fn select_claude_cli_credential(
fn select_claude_messages_credential(
bundle: &GatewayCredentialBundle,
) -> Option<GatewayPrimaryCredential> {
first_provider_api_key(
bundle,
&[
GatewayCredentialCarrier::XApiKey,
GatewayCredentialCarrier::ApiKey,
GatewayCredentialCarrier::AuthorizationBearer,
GatewayCredentialCarrier::XApiKey,
GatewayCredentialCarrier::ApiKey,
],
)
.or_else(|| first_bearer_token(bundle))
.or_else(|| select_cookie_credential(bundle))
}
fn select_claude_chat_credential(
bundle: &GatewayCredentialBundle,
) -> Option<GatewayPrimaryCredential> {
first_provider_api_key(
bundle,
&[
GatewayCredentialCarrier::XApiKey,
GatewayCredentialCarrier::ApiKey,
],
)
.or_else(|| first_bearer_token(bundle))
@@ -410,7 +393,8 @@ mod tests {
);
headers.insert("x-api-key", "claude-key".parse().unwrap());
let extracted = extract_request_credentials(&headers, &uri("/v1/messages"), "claude:chat");
let extracted =
extract_request_credentials(&headers, &uri("/v1/messages"), "claude:messages");
assert_eq!(
extracted.primary,
Some(GatewayPrimaryCredential::ProviderApiKey {
@@ -428,7 +412,8 @@ mod tests {
"Bearer cli-token".parse().unwrap(),
);
let extracted = extract_request_credentials(&headers, &uri("/v1/messages"), "claude:cli");
let extracted =
extract_request_credentials(&headers, &uri("/v1/messages"), "claude:messages");
assert_eq!(
extracted.primary,
Some(GatewayPrimaryCredential::ProviderApiKey {
@@ -446,7 +431,7 @@ mod tests {
let extracted = extract_request_credentials(
&headers,
&uri("/v1beta/models?key=gemini-query"),
"gemini:chat",
"gemini:generate_content",
);
assert_eq!(
extracted.primary,

View File

@@ -660,7 +660,7 @@ fn contains_api_format_or_alias(items: &[String], target: &str) -> bool {
}
fn normalize_api_format_alias(value: &str) -> String {
crate::ai_pipeline::normalize_legacy_openai_format_alias(value)
crate::ai_pipeline::normalize_api_format_alias(value)
}
fn api_format_matches(left: &str, right: &str) -> bool {

View File

@@ -54,21 +54,22 @@ pub(super) fn classify_ai_public_route(
"ai_public",
"claude",
"count_tokens",
"claude:chat",
"claude:messages",
false,
))
} else if method == http::Method::POST && normalized_path == "/v1/messages" {
if is_claude_cli_request(headers) {
Some(classified("ai_public", "claude", "cli", "claude:cli", true))
let route_kind = if is_claude_cli_request(headers) {
"cli"
} else {
Some(classified(
"ai_public",
"claude",
"chat",
"claude:chat",
true,
))
}
"messages"
};
Some(classified(
"ai_public",
"claude",
route_kind,
"claude:messages",
true,
))
} else if normalized_path.starts_with("/v1/videos") {
Some(classified(
"ai_public",
@@ -87,13 +88,19 @@ pub(super) fn classify_ai_public_route(
true,
))
} else if is_gemini_cli_request(headers) {
Some(classified("ai_public", "gemini", "cli", "gemini:cli", true))
Some(classified(
"ai_public",
"gemini",
"cli",
"gemini:generate_content",
true,
))
} else {
Some(classified(
"ai_public",
"gemini",
"chat",
"gemini:chat",
"generate_content",
"gemini:generate_content",
true,
))
}
@@ -112,7 +119,7 @@ pub(super) fn classify_ai_public_route(
"ai_public",
"gemini",
"files",
"gemini:chat",
"gemini:files",
true,
))
} else {

View File

@@ -162,7 +162,7 @@ pub(super) fn detect_public_models_auth_signature(uri: &Uri, headers: &http::Hea
.is_some();
let has_anthropic_version = header_value_str(headers, "anthropic-version").is_some();
if has_claude_key && has_anthropic_version {
return "claude:chat".to_string();
return "claude:messages".to_string();
}
let has_gemini_key = header_value_str(headers, "x-goog-api-key").is_some()
@@ -171,11 +171,11 @@ pub(super) fn detect_public_models_auth_signature(uri: &Uri, headers: &http::Hea
.any(|(key, value)| key == "key" && !value.trim().is_empty())
});
if has_gemini_key {
return "gemini:chat".to_string();
return "gemini:generate_content".to_string();
}
if uri.path().starts_with("/v1beta/models") {
return "gemini:chat".to_string();
return "gemini:generate_content".to_string();
}
"openai:chat".to_string()

View File

@@ -15,7 +15,7 @@ fn classifies_claude_count_tokens_as_non_execution_runtime_public_route() {
assert_eq!(decision.route_kind.as_deref(), Some("count_tokens"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("claude:chat")
Some("claude:messages")
);
assert!(!decision.is_execution_runtime_candidate());
}
@@ -32,7 +32,7 @@ fn classifies_models_list_as_claude_when_headers_match() {
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("claude:chat")
Some("claude:messages")
);
}
@@ -47,7 +47,7 @@ fn classifies_claude_messages_cli_when_bearer_without_api_key() {
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("claude:cli")
Some("claude:messages")
);
assert!(decision.is_execution_runtime_candidate());
}
@@ -66,23 +66,23 @@ fn classifies_claude_messages_cli_when_bearer_is_present_even_with_api_key() {
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("claude:cli")
Some("claude:messages")
);
assert!(decision.is_execution_runtime_candidate());
}
#[test]
fn classifies_claude_messages_chat_when_api_key_without_bearer() {
fn classifies_claude_messages_when_api_key_without_bearer() {
let headers = headers(&[("x-api-key", "sk-client")]);
let uri: Uri = "/v1/messages".parse().expect("uri should parse");
let decision =
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_family.as_deref(), Some("claude"));
assert_eq!(decision.route_kind.as_deref(), Some("chat"));
assert_eq!(decision.route_kind.as_deref(), Some("messages"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("claude:chat")
Some("claude:messages")
);
assert!(decision.is_execution_runtime_candidate());
}
@@ -100,7 +100,7 @@ fn classifies_gemini_cli_generate_content_when_x_app_contains_cli() {
assert_eq!(decision.route_kind.as_deref(), Some("cli"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("gemini:cli")
Some("gemini:generate_content")
);
assert!(decision.is_execution_runtime_candidate());
}

View File

@@ -33,7 +33,7 @@ fn classifies_v1beta_models_as_gemini_public_support_route() {
assert_eq!(decision.route_kind.as_deref(), Some("list"));
assert_eq!(
decision.auth_endpoint_signature.as_deref(),
Some("gemini:chat")
Some("gemini:generate_content")
);
}