mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Normalize canonical API formats
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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, ®ion, 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user