use std::time::Duration; use aether_contracts::{ExecutionPlan, ExecutionResult, ProxySnapshot}; use aether_data_contracts::repository::candidates::{ StoredRequestCandidate, UpsertRequestCandidateRecord, }; use aether_data_contracts::repository::global_models::{ AdminGlobalModelListQuery, AdminProviderModelListQuery, StoredAdminGlobalModelPage, StoredAdminProviderModel, UpsertAdminProviderModelRecord, }; use aether_data_contracts::repository::provider_catalog::{ ProviderCatalogUpstreamMetadataNamespaceUpdate, StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider, }; use aether_data_contracts::repository::quota::StoredProviderQuotaSnapshot; use aether_model_fetch::{ aggregate_models_for_cache, build_antigravity_load_code_assist_plan, fetch_models_from_transports, merge_upstream_metadata, model_fetch_interval_minutes, ModelFetchAssociationStore, ModelFetchTransportRuntime, }; use aether_scheduler_core::SchedulerAffinityTarget; use async_trait::async_trait; use serde_json::Value; use tracing::{debug, warn}; use super::{AppState, GatewayError}; use crate::clock::current_unix_secs; use crate::model_fetch::{CodexCatalogRuntime, ModelFetchRuntimeState}; use crate::provider_transport::{GatewayProviderTransportSnapshot, LocalResolvedOAuthRequestAuth}; use crate::request_candidate_runtime::{ RequestCandidateRuntimeCapabilityReader, RequestCandidateRuntimeReader, RequestCandidateRuntimeWriter, }; use crate::scheduler::state::SchedulerRuntimeState; use crate::{execution_runtime, provider_transport}; const MODEL_FETCH_RESPONSE_BODY_LIMIT_BYTES: usize = 8 * 1024 * 1024; impl AppState { pub(crate) async fn hydrate_antigravity_project_metadata_for_transport( &self, transport: &GatewayProviderTransportSnapshot, ) -> Option { if !provider_transport::antigravity::is_antigravity_provider_transport(transport) { return None; } if matches!( provider_transport::antigravity::resolve_local_antigravity_request_auth(transport), provider_transport::antigravity::AntigravityRequestAuthSupport::Supported(_) ) { return Some(transport.clone()); } let plan = match build_antigravity_load_code_assist_plan(self, transport).await { Ok(plan) => plan, Err(err) => { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, error = %err, "antigravity project metadata hydration failed" ); return None; } }; let result = match execution_runtime::execute_execution_runtime_sync_plan(self, None, &plan).await { Ok(result) => result, Err(err) => { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, error = ?err, "antigravity project metadata hydration request failed" ); return None; } }; if !(200..300).contains(&result.status_code) { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, status_code = result.status_code, "antigravity project metadata hydration returned non-success status" ); return None; } let Some(project_id) = result .body .as_ref() .and_then(|body| body.json_body.as_ref()) .and_then(extract_antigravity_load_code_assist_project_id) else { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, "antigravity project metadata hydration response missing project" ); return None; }; let upstream_metadata = serde_json::json!({ "antigravity": { "project_id": project_id, "updated_at": current_unix_secs(), } }); let merged_metadata = merge_upstream_metadata(transport.key.upstream_metadata.as_ref(), &upstream_metadata); let mut hydrated = transport.clone(); hydrated.key.upstream_metadata = Some(merged_metadata.clone()); if !matches!( provider_transport::antigravity::resolve_local_antigravity_request_auth(&hydrated), provider_transport::antigravity::AntigravityRequestAuthSupport::Supported(_) ) { return None; } if let Err(err) = self .update_provider_catalog_key_upstream_metadata( &transport.key.id, Some(&merged_metadata), Some(current_unix_secs()), ) .await { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, error = ?err, "antigravity project metadata hydration could not persist metadata" ); } Some(hydrated) } pub(crate) async fn hydrate_gemini_cli_project_metadata_for_transport( &self, transport: &GatewayProviderTransportSnapshot, ) -> Option { if !provider_transport::is_gemini_cli_provider_transport(transport) { return None; } if provider_transport::resolve_gemini_cli_project_id(transport).is_some() { return Some(transport.clone()); } let outcome = match fetch_models_from_transports(self, std::slice::from_ref(transport)).await { Ok(outcome) => outcome, Err(err) => { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, error = %err, "gemini_cli project metadata hydration failed" ); return None; } }; let upstream_metadata = outcome.upstream_metadata.as_ref()?; let merged_metadata = merge_upstream_metadata(transport.key.upstream_metadata.as_ref(), upstream_metadata); let mut hydrated = transport.clone(); hydrated.key.upstream_metadata = Some(merged_metadata.clone()); if provider_transport::resolve_gemini_cli_project_id(&hydrated).is_none() { return None; } if let Err(err) = self .update_provider_catalog_key_upstream_metadata( &transport.key.id, Some(&merged_metadata), Some(current_unix_secs()), ) .await { warn!( provider_id = %transport.provider.id, endpoint_id = %transport.endpoint.id, key_id = %transport.key.id, error = ?err, "gemini_cli project metadata hydration could not persist metadata" ); } Some(hydrated) } } fn extract_antigravity_load_code_assist_project_id(value: &Value) -> Option { let raw = value .get("cloudaicompanionProject") .or_else(|| value.get("cloudAiCompanionProject"))?; if let Some(project_id) = raw .as_str() .map(str::trim) .filter(|value| !value.is_empty()) { return Some(project_id.to_string()); } raw.as_object() .and_then(|object| { object .get("id") .or_else(|| object.get("project_id")) .or_else(|| object.get("projectId")) }) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) } #[async_trait] impl provider_transport::TransportTunnelAffinityLookup for AppState { async fn lookup_tunnel_attachment_owner( &self, node_id: &str, ) -> Result, String> { self.tunnel .lookup_attachment_owner(self.data.as_ref(), node_id) .await .map(|owner| { owner.map(|owner| provider_transport::TransportTunnelAttachmentOwner { gateway_instance_id: owner.gateway_instance_id, relay_base_url: owner.relay_base_url, observed_at_unix_secs: owner.observed_at_unix_secs, }) }) } } #[async_trait] impl provider_transport::VideoTaskTransportSnapshotLookup for AppState { async fn read_video_task_provider_transport_snapshot( &self, provider_id: &str, endpoint_id: &str, key_id: &str, ) -> Result, String> { self.read_provider_transport_snapshot(provider_id, endpoint_id, key_id) .await .map_err(GatewayError::into_message) } } #[async_trait] impl ModelFetchTransportRuntime for AppState { async fn resolve_local_oauth_request_auth( &self, transport: &GatewayProviderTransportSnapshot, ) -> Result, String> { AppState::resolve_local_oauth_request_auth(self, transport) .await .map_err(GatewayError::into_message) } async fn resolve_model_fetch_proxy( &self, transport: &GatewayProviderTransportSnapshot, ) -> Option { self.resolve_transport_proxy_snapshot_with_tunnel_affinity(transport) .await } async fn execute_model_fetch_execution_plan( &self, plan: &ExecutionPlan, ) -> Result { let bounded_plan = execution_runtime::transport::with_upstream_response_body_limit( plan, MODEL_FETCH_RESPONSE_BODY_LIMIT_BYTES, ); execution_runtime::execute_execution_runtime_sync_plan(self, None, &bounded_plan) .await .map_err(GatewayError::into_message) } } #[async_trait] impl CodexCatalogRuntime for AppState { fn codex_catalog_runtime_state(&self) -> &aether_runtime_state::RuntimeState { self.runtime_state.as_ref() } async fn read_codex_catalog_transport_snapshot( &self, provider_id: &str, endpoint_id: &str, key_id: &str, ) -> Result, String> { self.read_provider_transport_snapshot(provider_id, endpoint_id, key_id) .await .map_err(GatewayError::into_message) } async fn read_codex_catalog_credential_scope_strong( &self, provider_id: &str, key_id: &str, ) -> Result, String> { let Some(key) = self .list_provider_catalog_keys_by_ids_strong(&[key_id.to_string()]) .await .map_err(GatewayError::into_message)? .into_iter() .find(|key| key.id == key_id && key.provider_id == provider_id && key.is_active) else { return Ok(None); }; if let Some(scope) = crate::model_fetch::codex_catalog_credential_scope_from_stored_key(&key, None, None) { return Ok(Some(scope)); } let decrypted_auth_config = match key.encrypted_auth_config.as_deref() { Some(ciphertext) => Some( crate::handlers::shared::decrypt_catalog_secret_with_fallbacks( self.encryption_key(), ciphertext, ) .ok_or_else(|| { "Codex catalog auth config could not be verified for credential fencing" .to_string() })?, ), None => None, }; let decrypted_api_key = match key.encrypted_api_key.as_deref() { Some(ciphertext) => Some( crate::handlers::shared::decrypt_catalog_secret_with_fallbacks( self.encryption_key(), ciphertext, ) .ok_or_else(|| { "Codex catalog API key could not be verified for credential fencing".to_string() })?, ), None => None, }; Ok( crate::model_fetch::codex_catalog_credential_scope_from_stored_key( &key, decrypted_auth_config.as_deref(), decrypted_api_key.as_deref(), ), ) } } #[async_trait] impl ModelFetchRuntimeState for AppState { fn has_provider_catalog_data_reader(&self) -> bool { AppState::has_provider_catalog_data_reader(self) } fn has_provider_catalog_data_writer(&self) -> bool { AppState::has_provider_catalog_data_writer(self) } async fn list_provider_catalog_providers( &self, active_only: bool, ) -> Result, GatewayError> { AppState::list_provider_catalog_providers(self, active_only).await } async fn list_provider_catalog_endpoints_by_provider_ids( &self, provider_ids: &[String], ) -> Result, GatewayError> { AppState::list_provider_catalog_endpoints_by_provider_ids(self, provider_ids).await } async fn read_provider_transport_snapshot( &self, provider_id: &str, endpoint_id: &str, key_id: &str, ) -> Result, GatewayError> { AppState::read_provider_transport_snapshot(self, provider_id, endpoint_id, key_id).await } async fn execute_execution_runtime_sync_plan( &self, plan: &ExecutionPlan, ) -> Result { execution_runtime::execute_execution_runtime_sync_plan(self, None, plan).await } async fn read_recent_codex_catalog_client_version( &self, provider_id: &str, key_id: &str, ) -> Option { crate::model_fetch::read_codex_management_catalog(self, provider_id, key_id) .await .map(|catalog| catalog.client_version) } async fn update_provider_catalog_key_model_fetch_state( &self, key_id: &str, allowed_models: Option<&Value>, last_models_fetch_at_unix_secs: Option, last_models_fetch_error: Option<&str>, updated_at_unix_secs: Option, ) -> Result<(), GatewayError> { AppState::update_provider_catalog_key_model_fetch_state( self, key_id, allowed_models, last_models_fetch_at_unix_secs, last_models_fetch_error, updated_at_unix_secs, ) .await?; Ok(()) } async fn update_provider_catalog_key_model_fetch_success( &self, key_id: &str, allowed_models: Option<&Value>, last_models_fetch_at_unix_secs: u64, upstream_metadata_updates: &[ProviderCatalogUpstreamMetadataNamespaceUpdate], updated_at_unix_secs: Option, ) -> Result<(), GatewayError> { AppState::update_provider_catalog_key_model_fetch_success( self, key_id, allowed_models, last_models_fetch_at_unix_secs, upstream_metadata_updates, updated_at_unix_secs, ) .await?; Ok(()) } async fn write_upstream_models_cache( &self, provider_id: &str, key_id: &str, cached_models: &[Value], ) { let models = aggregate_models_for_cache(cached_models); if models.is_empty() { return; } let Ok(serialized) = serde_json::to_string(&models) else { return; }; let cache_key = format!("upstream_models:{provider_id}:{key_id}"); if let Err(err) = self .runtime_state .kv_set( &cache_key, serialized, Some(std::time::Duration::from_secs( model_fetch_interval_minutes().saturating_mul(60), )), ) .await { debug!( provider_id = %provider_id, key_id = %key_id, error = %err, "gateway model fetch cache write failed" ); } } } #[async_trait] impl ModelFetchAssociationStore for AppState { type Error = String; fn has_global_model_reader(&self) -> bool { self.data.has_global_model_reader() } fn has_global_model_writer(&self) -> bool { self.data.has_global_model_writer() } fn model_fetch_internal_error(&self, message: String) -> Self::Error { message } async fn list_admin_provider_models( &self, query: &AdminProviderModelListQuery, ) -> Result, Self::Error> { AppState::list_admin_provider_models(self, query) .await .map_err(|err| format!("{err:?}")) } async fn list_admin_global_models( &self, query: &AdminGlobalModelListQuery, ) -> Result { AppState::list_admin_global_models(self, query) .await .map_err(|err| format!("{err:?}")) } async fn create_admin_provider_model( &self, record: &UpsertAdminProviderModelRecord, ) -> Result, Self::Error> { AppState::create_admin_provider_model(self, record) .await .map_err(|err| format!("{err:?}")) } async fn list_provider_catalog_keys_by_provider_ids( &self, provider_ids: &[String], ) -> Result, Self::Error> { AppState::list_provider_catalog_keys_by_provider_ids(self, provider_ids) .await .map_err(|err| format!("{err:?}")) } } #[async_trait] impl RequestCandidateRuntimeReader for AppState { async fn read_request_candidates_by_request_id( &self, request_id: &str, ) -> Result, GatewayError> { AppState::read_request_candidates_by_request_id(self, request_id).await } } #[async_trait] impl RequestCandidateRuntimeCapabilityReader for AppState { async fn read_request_candidate_user_model_capability_settings( &self, user_id: &str, ) -> Result, GatewayError> { AppState::read_user_model_capability_settings(self, user_id).await } async fn read_request_candidate_api_key_force_capabilities( &self, user_id: &str, api_key_id: &str, ) -> Result, GatewayError> { AppState::read_auth_api_key_force_capabilities(self, user_id, api_key_id).await } } #[async_trait] impl RequestCandidateRuntimeWriter for AppState { fn has_request_candidate_data_writer(&self) -> bool { AppState::has_request_candidate_data_writer(self) } async fn upsert_request_candidate( &self, candidate: UpsertRequestCandidateRecord, ) -> Result, GatewayError> { AppState::upsert_request_candidate(self, candidate).await } async fn enqueue_request_candidate_status( &self, candidate: UpsertRequestCandidateRecord, ) -> Result, GatewayError> { AppState::enqueue_request_candidate_status(self, candidate).await } fn try_enqueue_request_candidate_status( &self, candidate: UpsertRequestCandidateRecord, ) -> Result<(), UpsertRequestCandidateRecord> { AppState::try_enqueue_request_candidate_status(self, candidate) } } #[async_trait] impl SchedulerRuntimeState for AppState { async fn read_provider_quota_snapshot( &self, provider_id: &str, ) -> Result, GatewayError> { AppState::read_provider_quota_snapshot(self, provider_id).await } async fn read_provider_catalog_providers_by_ids( &self, provider_ids: &[String], ) -> Result, GatewayError> { AppState::read_provider_catalog_providers_by_ids(self, provider_ids).await } async fn read_provider_catalog_keys_by_ids( &self, key_ids: &[String], ) -> Result, GatewayError> { AppState::read_provider_catalog_keys_by_ids(self, key_ids).await } async fn read_recent_request_candidates( &self, limit: usize, ) -> Result, GatewayError> { AppState::read_recent_request_candidates(self, limit).await } fn provider_key_rpm_reset_at(&self, key_id: &str, now_unix_secs: u64) -> Option { AppState::provider_key_rpm_reset_at(self, key_id, now_unix_secs) } fn read_cached_scheduler_affinity_target( &self, cache_key: &str, ttl: Duration, ) -> Option { AppState::read_scheduler_affinity_target(self, cache_key, ttl) } fn scheduler_affinity_epoch(&self) -> u64 { AppState::scheduler_affinity_epoch(self) } fn remember_scheduler_affinity_target( &self, cache_key: &str, target: SchedulerAffinityTarget, ttl: Duration, max_entries: usize, ) { AppState::remember_scheduler_affinity_target(self, cache_key, target, ttl, max_entries); } fn remember_scheduler_affinity_target_for_epoch( &self, cache_key: &str, target: SchedulerAffinityTarget, ttl: Duration, max_entries: usize, expected_epoch: Option, ) -> bool { AppState::remember_scheduler_affinity_target_for_epoch( self, cache_key, target, ttl, max_entries, expected_epoch, ) } }