fix: hydrate Gemini CLI project metadata

This commit is contained in:
Mas0nShi
2026-05-21 17:10:17 +08:00
parent e53d5f07e8
commit 3e6ce6cf4a
7 changed files with 386 additions and 36 deletions
@@ -29,6 +29,7 @@ pub(super) struct AdminProviderOAuthBatchImportEntry {
pub user_id: Option<String>,
pub email: Option<String>,
pub account_name: Option<String>,
pub project_id: Option<String>,
pub sso_rw_token: Option<String>,
pub cf_cookies: Option<String>,
pub cf_clearance: Option<String>,
@@ -74,6 +75,20 @@ fn coerce_admin_provider_oauth_import_str(value: Option<&serde_json::Value>) ->
.map(ToOwned::to_owned)
}
fn coerce_admin_provider_oauth_import_project_id(
value: Option<&serde_json::Value>,
) -> Option<String> {
match value {
Some(serde_json::Value::Object(object)) => coerce_admin_provider_oauth_import_str(
object
.get("id")
.or_else(|| object.get("project_id"))
.or_else(|| object.get("projectId")),
),
other => coerce_admin_provider_oauth_import_str(other),
}
}
fn grok_cookie_value(raw: &str, name: &str) -> Option<String> {
raw.trim()
.strip_prefix("Cookie:")
@@ -151,6 +166,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
user_id: grok_cookie_value(raw_token, "x-userid"),
email: None,
account_name: None,
project_id: None,
sso_rw_token: grok_cookie_value(raw_token, "sso-rw"),
cf_cookies: grok_cookie_profile(raw_token),
cf_clearance: grok_cookie_value(raw_token, "cf_clearance"),
@@ -250,6 +266,13 @@ fn extract_admin_provider_oauth_batch_import_entry(
.get("account_name")
.or_else(|| object.get("accountName")),
);
let project_id = coerce_admin_provider_oauth_import_project_id(
object
.get("project_id")
.or_else(|| object.get("projectId"))
.or_else(|| object.get("cloudaicompanionProject"))
.or_else(|| object.get("cloudAiCompanionProject")),
);
let sso_rw_token = coerce_admin_provider_oauth_import_str(
object
.get("sso_rw_token")
@@ -295,6 +318,7 @@ fn extract_admin_provider_oauth_batch_import_entry(
user_id,
email,
account_name,
project_id,
sso_rw_token,
cf_cookies,
cf_clearance,
@@ -363,6 +387,19 @@ pub(super) fn apply_admin_provider_oauth_batch_import_hints(
auth_config: &mut serde_json::Map<String, serde_json::Value>,
) {
let provider_type = provider_type.trim().to_ascii_lowercase();
if provider_type == "gemini_cli" {
if let Some(project_id) = entry.project_id.as_ref() {
auth_config
.entry("project_id".to_string())
.or_insert_with(|| json!(project_id));
}
if let Some(plan_type) = entry.plan_type.as_ref() {
auth_config
.entry("plan_type".to_string())
.or_insert_with(|| json!(plan_type));
}
return;
}
if !matches!(provider_type.as_str(), "codex" | "chatgpt_web" | "grok") {
return;
}
@@ -513,7 +550,10 @@ pub(super) fn build_admin_provider_oauth_batch_task_state(
#[cfg(test)]
mod tests {
use super::parse_admin_provider_oauth_batch_import_entries;
use super::{
apply_admin_provider_oauth_batch_import_hints,
parse_admin_provider_oauth_batch_import_entries,
};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use serde_json::json;
@@ -633,4 +673,36 @@ mod tests {
assert_eq!(entries[0].user_id.as_deref(), Some("user-1"));
assert_eq!(entries[0].pool_tier.as_deref(), Some("heavy"));
}
#[test]
fn parses_gemini_cli_project_id_hint() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"gemini_cli",
r#"[{"refresh_token":"rt-1","projectId":"project-gemini-cli-1","planType":"free"}]"#,
);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].refresh_token.as_deref(), Some("rt-1"));
assert_eq!(
entries[0].project_id.as_deref(),
Some("project-gemini-cli-1")
);
assert_eq!(entries[0].plan_type.as_deref(), Some("free"));
}
#[test]
fn applies_gemini_cli_project_id_hint_to_auth_config() {
let entries = parse_admin_provider_oauth_batch_import_entries(
"gemini_cli",
r#"{"refreshToken":"rt-1","cloudaicompanionProject":{"id":"project-gemini-cli-2"}}"#,
);
let mut auth_config = serde_json::Map::new();
apply_admin_provider_oauth_batch_import_hints("gemini_cli", &entries[0], &mut auth_config);
assert_eq!(
auth_config.get("project_id"),
Some(&json!("project-gemini-cli-2"))
);
}
}
@@ -66,7 +66,7 @@ pub(crate) async fn refresh_gemini_cli_provider_quota_locally(
let mut auto_removed_count = 0usize;
for key in keys {
let transport = match state
let mut transport = match state
.read_provider_transport_snapshot(&provider.id, &endpoint.id, &key.id)
.await?
{
@@ -102,14 +102,27 @@ pub(crate) async fn refresh_gemini_cli_provider_quota_locally(
}
};
let Some(project_id) = crate::provider_transport::resolve_gemini_cli_project_id(&transport)
else {
let project_id = match crate::provider_transport::resolve_gemini_cli_project_id(&transport)
{
Some(project_id) => Some(project_id),
None => state
.app()
.hydrate_gemini_cli_project_metadata_for_transport(&transport)
.await
.and_then(|hydrated| {
let project_id =
crate::provider_transport::resolve_gemini_cli_project_id(&hydrated);
transport = hydrated;
project_id
}),
};
let Some(project_id) = project_id else {
failed_count += 1;
results.push(json!({
"key_id": key.id,
"key_name": key.name,
"status": "error",
"message": "缺少 Gemini CLI project_id,请先刷新模型或在 auth_config/upstream_metadata 中写入 project_id",
"message": "缺少 Gemini CLI project_id,loadCodeAssist 未返回可用项目信息",
}));
continue;
};
@@ -15,7 +15,7 @@ use crate::ai_serving::{
ANTIGRAVITY_V1INTERNAL_ENVELOPE_NAME, GEMINI_CHAT_SYNC_FINALIZE_REPORT_KIND,
OPENAI_IMAGE_SYNC_FINALIZE_REPORT_KIND,
};
use crate::clock::current_unix_ms;
use crate::clock::{current_unix_ms, current_unix_secs};
use crate::execution_runtime;
use crate::handlers::admin::provider::shared::model_test_capabilities::{
admin_provider_model_supports_image_generation, admin_provider_model_test_capabilities_payload,
@@ -64,7 +64,7 @@ use aether_data_contracts::repository::provider_catalog::{
};
use aether_model_fetch::{
aggregate_models_for_cache, fetch_models_from_transports, json_string_list,
preset_models_for_provider, selected_models_fetch_endpoints,
merge_upstream_metadata, preset_models_for_provider, selected_models_fetch_endpoints,
};
use axum::{
body::{to_bytes, Body},
@@ -509,6 +509,18 @@ async fn provider_query_fetch_models_for_key(
)
.await;
}
if let Some(upstream_metadata) = outcome.upstream_metadata.as_ref() {
let merged_metadata =
merge_upstream_metadata(key.upstream_metadata.as_ref(), upstream_metadata);
state
.app()
.update_provider_catalog_key_upstream_metadata(
&key.id,
Some(&merged_metadata),
Some(current_unix_secs()),
)
.await?;
}
if unique_models.is_empty() && !all_errors.is_empty() {
if let Some(fallback) = provider_query_codex_preset_fallback(provider) {
@@ -2639,7 +2639,7 @@ async fn provider_query_execute_standard_test_candidate(
route_path: &str,
trace_id: &str,
) -> Result<ProviderQueryExecutionOutcome, GatewayError> {
let Some(transport) = state
let Some(mut transport) = state
.read_provider_transport_snapshot(&provider.id, &candidate.endpoint.id, &candidate.key.id)
.await?
else {
@@ -2839,8 +2839,21 @@ async fn provider_query_execute_standard_test_candidate(
if crate::provider_transport::is_gemini_cli_provider_transport(&transport)
&& normalized_provider_api_format == "gemini:generate_content"
{
let Some(project_id) = crate::provider_transport::resolve_gemini_cli_project_id(&transport)
else {
let project_id = match crate::provider_transport::resolve_gemini_cli_project_id(&transport)
{
Some(project_id) => Some(project_id),
None => state
.app()
.hydrate_gemini_cli_project_metadata_for_transport(&transport)
.await
.and_then(|hydrated| {
let project_id =
crate::provider_transport::resolve_gemini_cli_project_id(&hydrated);
transport = hydrated;
project_id
}),
};
let Some(project_id) = project_id else {
return Ok(provider_query_skipped_execution_outcome(
provider_request_body,
"Gemini CLI project_id is unavailable for v1internal request",