use crate::AppService; use crate::ai::types::{AiModelFilter, AiModelListItem, AiModelResponse}; use crate::error::AppError; use crate::{Pagination, session_user}; use db::sqlx; use db::sqlx::AssertSqlSafe; use model::ai::AiModelModel; use model::ai::AiProviderModel; use session::Session; impl AppService { pub async fn ai_model_list( &self, ctx: &Session, filter: AiModelFilter, pagination: Pagination, ) -> Result, AppError> { let _user_uid = session_user(ctx)?; let mut conditions = vec![ "m.public = true".to_string(), "m.deleted_at IS NULL".to_string(), ]; let mut param_idx = 1; if filter.enabled.is_some() { conditions.push(format!("m.enabled = ${param_idx}")); param_idx += 1; } if filter.provider.is_some() { conditions.push(format!("m.provider = ${param_idx}")); param_idx += 1; } if filter.modality.is_some() { conditions.push(format!("m.modality = ${param_idx}")); param_idx += 1; } if filter.name.is_some() { conditions.push(format!("m.name ILIKE ${param_idx}")); param_idx += 1; } let where_clause = conditions.join(" AND "); let limit_idx = param_idx; let offset_idx = param_idx + 1; let query = format!( "SELECT m.id, m.provider, m.name, m.display_name, m.description, m.modality, \ m.context_window, m.input_token_limit, m.output_token_limit, \ m.enabled, m.public, m.created_at, m.updated_at, m.deleted_at \ FROM ai_model m WHERE {where_clause} \ ORDER BY m.display_name ASC LIMIT ${limit_idx} OFFSET ${offset_idx}" ); let mut q = sqlx::query_as::<_, AiModelModel>(AssertSqlSafe(query)); if let Some(enabled) = &filter.enabled { q = q.bind(enabled); } if let Some(provider) = &filter.provider { q = q.bind(provider); } if let Some(modality) = &filter.modality { q = q.bind(modality); } if let Some(name) = &filter.name { q = q.bind(format!("%{}%", name)); } q = q .bind(pagination.limit() as i64) .bind(pagination.offset() as i64); let models = q .fetch_all(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let mut results = Vec::new(); for m in models { let provider = self.ai_provider_by_id(m.provider).await?; results.push(AiModelListItem { id: m.id, name: m.name, display_name: m.display_name, description: crate::non_empty( m.description.unwrap_or_default(), ), modality: m.modality, provider_name: provider.name, provider_logo_url: provider.logo_url, context_window: m.context_window, input_token_limit: m.input_token_limit, output_token_limit: m.output_token_limit, enabled: m.enabled, created_at: m.created_at, updated_at: m.updated_at, }); } Ok(results) } pub async fn ai_model_get( &self, ctx: &Session, id: uuid::Uuid, ) -> Result { let _user_uid = session_user(ctx)?; let m = sqlx::query_as::<_, AiModelModel>( "SELECT id, provider, name, display_name, description, modality, \ context_window, input_token_limit, output_token_limit, \ enabled, public, created_at, updated_at, deleted_at \ FROM ai_model WHERE id = $1 AND (public = true OR deleted_at IS NULL)", ) .bind(id) .fetch_optional(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::NotFound("model not found".to_string()))?; let provider = self.ai_provider_by_id(m.provider).await?; let card = self.ai_card_get_inner(m.id).await?; let versions = self.ai_version_list_inner(m.id).await?; let tags = self.ai_tag_list_inner(m.id).await?; let like_count = self.ai_like_count_inner(m.id).await?; Ok(AiModelResponse { id: m.id, name: m.name, display_name: m.display_name, description: crate::non_empty(m.description.unwrap_or_default()), modality: m.modality, context_window: m.context_window, input_token_limit: m.input_token_limit, output_token_limit: m.output_token_limit, enabled: m.enabled, public: m.public, provider_name: provider.name, provider_logo_url: provider.logo_url, card, versions, tags, like_count, created_at: m.created_at, updated_at: m.updated_at, }) } pub(crate) async fn ai_provider_by_id( &self, id: uuid::Uuid, ) -> Result { sqlx::query_as::<_, AiProviderModel>( "SELECT id, name, base_url, website_url, logo_url, enabled, created_at, updated_at \ FROM ai_provider WHERE id = $1", ) .bind(id) .fetch_one(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string())) } }