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

@@ -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