Merge commit 'refs/pr/585'

# Conflicts:
#	apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs
#	apps/aether-gateway/src/tests/ai_execute/stream_provider_gemini/local_cli.rs
#	apps/aether-gateway/src/tests/ai_execute/sync/gemini/cli.rs
#	apps/aether-gateway/src/tests/control/admin/provider_query.rs
#	crates/aether-provider-transport/src/gemini_cli/mod.rs
#	crates/aether-provider-transport/src/gemini_cli/request.rs
#	crates/aether-provider-transport/src/gemini_cli/url.rs
#	crates/aether-provider-transport/src/lib.rs
This commit is contained in:
fawney19
2026-05-28 13:19:42 +08:00
21 changed files with 867 additions and 404 deletions
@@ -452,10 +452,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
struct SeenExecutionRuntimeStreamRequest {
trace_id: String,
url: String,
has_model_field: bool,
project: String,
outer_model: String,
user_prompt_id: String,
envelope_model: String,
inner_model_present: bool,
accept: String,
authorization: String,
exact_temperature: f64,
@@ -587,7 +587,7 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
{"action":"drop","path":"toolConfig"}
])),
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:streamGenerateContent".to_string()),
None,
None,
None,
None,
@@ -733,15 +733,10 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
has_model_field: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.is_some(),
project: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("project"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -752,10 +747,16 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
envelope_model: payload
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
project: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("project"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -939,19 +940,19 @@ async fn gateway_executes_gemini_cli_stream_via_local_decision_gate_after_oauth_
seen_execution_runtime_request.url,
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
);
assert!(seen_execution_runtime_request.has_model_field);
assert_eq!(
seen_execution_runtime_request.project,
"gemini-cli-project-1"
);
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-gemini-cli-oauth-local-stream-123"
);
assert_eq!(
seen_execution_runtime_request.envelope_model,
"gemini-cli-upstream"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(seen_execution_runtime_request.accept, "text/event-stream");
assert_eq!(
seen_execution_runtime_request.authorization,
@@ -2010,7 +2010,9 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
url: String,
model: String,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
auth_header_value: String,
endpoint_tag: String,
has_contents: bool,
@@ -2031,7 +2033,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
"local".to_string(),
true,
false,
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!(["gpt-5"])),
api_key_id.to_string(),
@@ -2042,7 +2044,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
Some(60),
Some(5),
Some(4_102_444_800),
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:chat"])),
Some(serde_json::json!(["gpt-5"])),
)
@@ -2052,8 +2054,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
fn sample_candidate_row() -> StoredMinimalCandidateSelectionRow {
StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-chat-gemini-cli-local-1".to_string(),
provider_name: "gemini".to_string(),
provider_type: "custom".to_string(),
provider_name: "gemini_cli".to_string(),
provider_type: "gemini_cli".to_string(),
provider_priority: 10,
provider_is_active: true,
endpoint_id: "endpoint-openai-chat-gemini-cli-local-1".to_string(),
@@ -2062,8 +2064,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
endpoint_kind: Some("cli".to_string()),
endpoint_is_active: true,
key_id: "key-openai-chat-gemini-cli-local-1".to_string(),
key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(),
key_name: "oauth".to_string(),
key_auth_type: "oauth".to_string(),
key_is_active: true,
key_api_formats: Some(vec!["gemini:generate_content".to_string()]),
key_allowed_models: None,
@@ -2091,9 +2093,9 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-chat-gemini-cli-local-1".to_string(),
"gemini".to_string(),
"gemini_cli".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
"gemini_cli".to_string(),
)
.expect("provider should build")
.with_transport_fields(
@@ -2120,13 +2122,13 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"openai-chat-gemini-cli-cross-format"}
])),
None,
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -2138,8 +2140,8 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
StoredProviderCatalogKey::new(
"key-openai-chat-gemini-cli-local-1".to_string(),
"provider-openai-chat-gemini-cli-local-1".to_string(),
"prod".to_string(),
"bearer".to_string(),
"oauth".to_string(),
"oauth".to_string(),
None,
true,
)
@@ -2241,13 +2243,26 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
model: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
auth_header_value: payload
.get("headers")
.and_then(|value| value.get("authorization"))
@@ -2263,6 +2278,7 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
has_contents: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("contents"))
.is_some(),
});
@@ -2336,15 +2352,20 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
let response_status = response.status();
let execution_path = response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
let response_body = response.text().await.expect("body should read");
assert_eq!(
response
.headers()
.get(EXECUTION_PATH_HEADER)
.and_then(|value| value.to_str().ok()),
execution_path.as_deref(),
Some(EXECUTION_PATH_EXECUTION_RUNTIME_SYNC)
);
let response_json: serde_json::Value = response.json().await.expect("body should parse");
assert_eq!(response_status, StatusCode::TOO_MANY_REQUESTS);
let response_json: serde_json::Value =
serde_json::from_str(&response_body).expect("body should parse");
assert_eq!(
response_json,
json!({
@@ -2367,9 +2388,17 @@ async fn gateway_returns_openai_chat_error_for_local_cross_format_gemini_cli_syn
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(seen_execution_runtime_request.model, "gemini-cli-upstream");
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-openai-chat-gemini-cli-local-error-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(
seen_execution_runtime_request.auth_header_value,
"Bearer sk-upstream-openai-chat-gemini-cli"
@@ -1572,7 +1572,7 @@ async fn gateway_returns_openai_responses_error_for_local_sync_failure() {
}
#[tokio::test]
async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sync_failure() {
async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_cli_sync_failure() {
#[derive(Debug, Clone)]
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
@@ -1580,7 +1580,9 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
authorization: String,
endpoint_tag: String,
has_contents: bool,
model: String,
outer_model: String,
user_prompt_id: String,
inner_model_present: bool,
}
fn hash_api_key(value: &str) -> String {
@@ -1598,7 +1600,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
"local".to_string(),
true,
false,
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:responses"])),
Some(serde_json::json!(["gpt-5"])),
api_key_id.to_string(),
@@ -1609,7 +1611,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
Some(60),
Some(5),
Some(4_102_444_800_i64),
Some(serde_json::json!(["openai", "gemini"])),
Some(serde_json::json!(["openai", "gemini", "gemini_cli"])),
Some(serde_json::json!(["openai:responses"])),
Some(serde_json::json!(["gpt-5"])),
)
@@ -1619,8 +1621,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
fn sample_candidate_row() -> StoredMinimalCandidateSelectionRow {
StoredMinimalCandidateSelectionRow {
provider_id: "provider-openai-cli-gemini-local-1".to_string(),
provider_name: "gemini".to_string(),
provider_type: "custom".to_string(),
provider_name: "gemini_cli".to_string(),
provider_type: "gemini_cli".to_string(),
provider_priority: 10,
provider_is_active: true,
endpoint_id: "endpoint-openai-cli-gemini-local-1".to_string(),
@@ -1629,8 +1631,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
endpoint_kind: Some("cli".to_string()),
endpoint_is_active: true,
key_id: "key-openai-cli-gemini-local-1".to_string(),
key_name: "prod".to_string(),
key_auth_type: "bearer".to_string(),
key_name: "oauth".to_string(),
key_auth_type: "oauth".to_string(),
key_is_active: true,
key_api_formats: Some(vec!["gemini:generate_content".to_string()]),
key_allowed_models: None,
@@ -1658,9 +1660,9 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
"provider-openai-cli-gemini-local-1".to_string(),
"gemini".to_string(),
"gemini_cli".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
"gemini_cli".to_string(),
)
.expect("provider should build")
.with_transport_fields(
@@ -1687,13 +1689,13 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
)
.expect("endpoint should build")
.with_transport_fields(
"https://generativelanguage.googleapis.com".to_string(),
"https://cloudcode-pa.googleapis.com".to_string(),
Some(serde_json::json!([
{"action":"set","key":"x-endpoint-tag","value":"openai-cli-gemini-cross-format"}
])),
None,
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -1705,8 +1707,8 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
StoredProviderCatalogKey::new(
"key-openai-cli-gemini-local-1".to_string(),
"provider-openai-cli-gemini-local-1".to_string(),
"prod".to_string(),
"bearer".to_string(),
"oauth".to_string(),
"oauth".to_string(),
None,
true,
)
@@ -1806,15 +1808,29 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
has_contents: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("contents"))
.is_some(),
model: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
user_prompt_id: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("user_prompt_id"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
});
Json(json!({
"request_id": "trace-openai-cli-gemini-local-error-123",
@@ -1917,7 +1933,7 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
);
assert_eq!(
seen_execution_runtime_request.url,
"https://generativelanguage.googleapis.com/custom/v1beta/models/gemini-cli-upstream:generateContent"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert_eq!(
seen_execution_runtime_request.authorization,
@@ -1928,7 +1944,15 @@ async fn gateway_returns_openai_responses_error_for_local_cross_format_gemini_sy
"openai-cli-gemini-cross-format"
);
assert!(seen_execution_runtime_request.has_contents);
assert_eq!(seen_execution_runtime_request.model, "gemini-cli-upstream");
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-openai-cli-gemini-local-error-123"
);
assert!(!seen_execution_runtime_request.inner_model_present);
let stored_candidates = request_candidate_repository
.list_by_request_id("trace-openai-cli-gemini-local-error-123")
@@ -779,10 +779,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
struct SeenExecutionRuntimeSyncRequest {
trace_id: String,
url: String,
has_model_field: bool,
project: String,
outer_model: String,
user_prompt_id: String,
envelope_model: String,
inner_model_present: bool,
authorization: String,
exact_temperature: f64,
endpoint_tag: String,
@@ -913,7 +913,7 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
{"action":"drop","path":"toolConfig"}
])),
Some(2),
Some("/custom/v1beta/models/gemini-cli-upstream:generateContent".to_string()),
None,
None,
None,
None,
@@ -1060,15 +1060,10 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
has_model_field: payload
outer_model: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("model"))
.is_some(),
project: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("project"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1079,10 +1074,16 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
envelope_model: payload
inner_model_present: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("request"))
.and_then(|value| value.get("model"))
.is_some(),
project: payload
.get("body")
.and_then(|value| value.get("json_body"))
.and_then(|value| value.get("project"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string(),
@@ -1273,21 +1274,21 @@ async fn gateway_executes_gemini_cli_sync_via_local_decision_gate_after_oauth_re
);
assert_eq!(
seen_execution_runtime_request.url,
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
"https://cloudcode-pa.googleapis.com/v1internal:generateContent"
);
assert!(seen_execution_runtime_request.has_model_field);
assert_eq!(
seen_execution_runtime_request.project,
"gemini-cli-project-1"
);
assert_eq!(
seen_execution_runtime_request.outer_model,
"gemini-cli-upstream"
);
assert_eq!(
seen_execution_runtime_request.user_prompt_id,
"trace-gemini-cli-oauth-local-sync-123"
);
assert_eq!(
seen_execution_runtime_request.envelope_model,
"gemini-cli-upstream"
);
assert!(!seen_execution_runtime_request.inner_model_present);
assert_eq!(
seen_execution_runtime_request.authorization,
"Bearer refreshed-gemini-cli-access-token"