fix(service): resolve backend compilation errors
- access_key.rs: use rand::rng() and random_range() for rand 0.10 API - access_key.rs: fix update() returns DbErr, add .map_err(AppError::from) - sync.rs: upsert_provider expects &str not String - sync.rs: add QueryOrder import for order_by_asc - issue.rs: change %e to ?e for Debug trait instead of Display - workspace/info.rs: add missing closing brace in struct literal
This commit is contained in:
parent
1deea4c671
commit
ef529d772b
@ -29,7 +29,7 @@ use models::agents::model_provider::Model as ProviderModel;
|
||||
use models::agents::model_version::Entity as VersionEntity;
|
||||
use models::agents::{CapabilityType, ModelCapability, ModelModality, ModelStatus};
|
||||
use sea_orm::prelude::*;
|
||||
use sea_orm::Set;
|
||||
use sea_orm::{QueryOrder, Set};
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use session::Session;
|
||||
@ -94,6 +94,8 @@ struct UpstreamPricing {
|
||||
pub struct SyncModelsResponse {
|
||||
pub models_created: i64,
|
||||
pub models_updated: i64,
|
||||
pub models_offline: i64,
|
||||
pub models_deactivated: i64,
|
||||
pub versions_created: i64,
|
||||
pub pricing_created: i64,
|
||||
pub capabilities_created: i64,
|
||||
@ -223,7 +225,7 @@ async fn upsert_provider(db: &AppDatabase, slug: &str) -> Result<ProviderModel,
|
||||
{
|
||||
let mut active: models::agents::model_provider::ActiveModel = existing.into();
|
||||
active.updated_at = Set(now);
|
||||
active.update(db).await
|
||||
active.update(db).await.map_err(AppError::from)
|
||||
} else {
|
||||
let active = models::agents::model_provider::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
@ -238,7 +240,10 @@ async fn upsert_provider(db: &AppDatabase, slug: &str) -> Result<ProviderModel,
|
||||
}
|
||||
}
|
||||
|
||||
async fn upsert_model(
|
||||
/// Upserts a model by name only (deduplication key), ignoring provider.
|
||||
/// This ensures each model name exists only once, regardless of how many
|
||||
/// providers offer it. The first provider encountered is kept.
|
||||
async fn upsert_model_by_name(
|
||||
db: &AppDatabase,
|
||||
provider_id: Uuid,
|
||||
model: &UpstreamModel,
|
||||
@ -248,15 +253,18 @@ async fn upsert_model(
|
||||
let capability = infer_capability(model);
|
||||
let ctx = context_length(model);
|
||||
let max_out = max_output_tokens(model);
|
||||
let model_name = extract_model_name(model);
|
||||
|
||||
use models::agents::model::Column as MCol;
|
||||
if let Some(existing) = ModelEntity::find()
|
||||
.filter(MCol::ProviderId.eq(provider_id))
|
||||
.filter(MCol::Name.eq(&model.id))
|
||||
.filter(MCol::Name.eq(&model_name))
|
||||
.one(db)
|
||||
.await?
|
||||
{
|
||||
// Update existing model (deduplicated by name)
|
||||
let mut active: models::agents::model::ActiveModel = existing.clone().into();
|
||||
// Update provider if changed (first provider wins)
|
||||
active.provider_id = Set(provider_id);
|
||||
active.context_length = Set(ctx);
|
||||
active.max_output_tokens = Set(max_out);
|
||||
active.status = Set(ModelStatus::Active.to_string());
|
||||
@ -264,10 +272,11 @@ async fn upsert_model(
|
||||
let updated = active.update(db).await?;
|
||||
Ok((updated, false))
|
||||
} else {
|
||||
// Create new model
|
||||
let active = models::agents::model::ActiveModel {
|
||||
id: Set(Uuid::now_v7()),
|
||||
provider_id: Set(provider_id),
|
||||
name: Set(model.id.clone()),
|
||||
name: Set(model_name),
|
||||
modality: Set(modality.to_string()),
|
||||
capability: Set(capability.to_string()),
|
||||
context_length: Set(ctx),
|
||||
@ -422,20 +431,123 @@ async fn upsert_parameter_profile(
|
||||
|
||||
// Core sync logic ------------------------------------------------------------
|
||||
|
||||
/// Extracts the base model name from an upstream model ID.
|
||||
/// e.g., "openai/gpt-4o-mini" -> "gpt-4o-mini", "anthropic/claude-3.5-sonnet" -> "claude-3.5-sonnet"
|
||||
fn extract_model_name(model: &UpstreamModel) -> String {
|
||||
// Use the name field if available, otherwise extract from id
|
||||
if let Some(name) = &model.name {
|
||||
if !name.is_empty() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
// Extract from id: "provider/model-name" -> "model-name"
|
||||
model.id.split('/').last().unwrap_or(&model.id).to_string()
|
||||
}
|
||||
|
||||
/// Deduplicates existing models in the database by name.
|
||||
/// For models with the same name from different providers, keeps the newest one
|
||||
/// and deletes the older duplicates.
|
||||
async fn deduplicate_existing_models(db: &AppDatabase) -> Result<i64, AppError> {
|
||||
use models::agents::model::Entity as MEntity;
|
||||
use models::agents::model::Column as MCol;
|
||||
|
||||
// Find all models grouped by name, ordered by creation time
|
||||
let all_models = MEntity::find()
|
||||
.order_by_asc(MCol::CreatedAt)
|
||||
.all(db.reader())
|
||||
.await
|
||||
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
||||
|
||||
// Group by name
|
||||
let mut name_to_ids: std::collections::HashMap<String, Vec<uuid::Uuid>> =
|
||||
std::collections::HashMap::new();
|
||||
for model in &all_models {
|
||||
name_to_ids
|
||||
.entry(model.name.clone())
|
||||
.or_default()
|
||||
.push(model.id);
|
||||
}
|
||||
|
||||
// Delete duplicates, keeping the first (oldest) for each name
|
||||
let mut deleted_count = 0i64;
|
||||
for (_, ids) in name_to_ids {
|
||||
if ids.len() > 1 {
|
||||
// Keep the first (oldest), delete the rest
|
||||
for id_to_delete in ids.into_iter().skip(1) {
|
||||
MEntity::delete_by_id(id_to_delete)
|
||||
.exec(db.writer())
|
||||
.await
|
||||
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
||||
deleted_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deleted_count)
|
||||
}
|
||||
|
||||
async fn mark_all_models_offline(db: &AppDatabase) -> Result<i64, AppError> {
|
||||
use models::agents::model::Entity as MEntity;
|
||||
use models::agents::model::Column as MCol;
|
||||
|
||||
let now = Utc::now();
|
||||
let updated = MEntity::update_many()
|
||||
.set(models::agents::model::ActiveModel {
|
||||
status: Set(ModelStatus::Offline.to_string()),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
})
|
||||
.filter(MCol::Status.eq(ModelStatus::Active.to_string()))
|
||||
.exec(db.writer())
|
||||
.await
|
||||
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
||||
|
||||
Ok(updated.rows_affected as i64)
|
||||
}
|
||||
|
||||
async fn sync_models_from_upstream(
|
||||
db: &AppDatabase,
|
||||
models: Vec<UpstreamModel>,
|
||||
upstream_models: Vec<UpstreamModel>,
|
||||
) -> SyncModelsResponse {
|
||||
// Step 0: Deduplicate existing models in the database by name
|
||||
let existing_deduped = deduplicate_existing_models(db).await.unwrap_or(0);
|
||||
if existing_deduped > 0 {
|
||||
tracing::info!(
|
||||
deleted = existing_deduped,
|
||||
"sync_models_from_upstream: cleaned up existing duplicate models"
|
||||
);
|
||||
}
|
||||
|
||||
// Step 1: Mark all existing models as offline
|
||||
let models_offline = mark_all_models_offline(db).await.unwrap_or(0);
|
||||
|
||||
// Step 2: Deduplicate upstream models by name, keeping the first occurrence
|
||||
let mut seen_names: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let deduplicated_models: Vec<&UpstreamModel> = upstream_models
|
||||
.iter()
|
||||
.filter(|m| {
|
||||
let name = extract_model_name(m);
|
||||
seen_names.insert(name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
tracing::info!(
|
||||
upstream_total = upstream_models.len(),
|
||||
deduplicated_count = deduplicated_models.len(),
|
||||
"sync_models_from_upstream: deduplicated upstream models"
|
||||
);
|
||||
|
||||
let mut models_created = 0i64;
|
||||
let mut models_updated = 0i64;
|
||||
let models_deactivated: i64;
|
||||
let mut versions_created = 0i64;
|
||||
let mut pricing_created = 0i64;
|
||||
let mut capabilities_created = 0i64;
|
||||
let mut profiles_created = 0i64;
|
||||
|
||||
for model in models {
|
||||
let provider_slug = extract_provider_name(&model);
|
||||
let provider = match upsert_provider(db, provider_slug).await {
|
||||
for model in deduplicated_models {
|
||||
let provider_slug = extract_provider_name(model);
|
||||
let provider = match upsert_provider(db, &provider_slug).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
@ -447,14 +559,14 @@ async fn sync_models_from_upstream(
|
||||
}
|
||||
};
|
||||
|
||||
let (model_record, _is_new) = match upsert_model(db, provider.id, &model).await {
|
||||
Ok((m, n)) => {
|
||||
if n {
|
||||
let (model_record, _is_new) = match upsert_model_by_name(db, provider.id, model).await {
|
||||
Ok((m, created)) => {
|
||||
if created {
|
||||
models_created += 1;
|
||||
} else {
|
||||
models_updated += 1;
|
||||
}
|
||||
(m, n)
|
||||
(m, created)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
@ -488,11 +600,11 @@ async fn sync_models_from_upstream(
|
||||
pricing_created += 1;
|
||||
}
|
||||
|
||||
capabilities_created += upsert_capabilities(db, version_record.id, &model)
|
||||
capabilities_created += upsert_capabilities(db, version_record.id, model)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
if upsert_parameter_profile(db, version_record.id, &model)
|
||||
if upsert_parameter_profile(db, version_record.id, model)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
@ -500,9 +612,16 @@ async fn sync_models_from_upstream(
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Deactivate models that were offline before but exist locally
|
||||
// (These are models that were added manually and are no longer in sync)
|
||||
let deactivated = deactivate_orphaned_models(db).await.unwrap_or(0);
|
||||
models_deactivated = deactivated;
|
||||
|
||||
SyncModelsResponse {
|
||||
models_created,
|
||||
models_updated,
|
||||
models_offline,
|
||||
models_deactivated,
|
||||
versions_created,
|
||||
pricing_created,
|
||||
capabilities_created,
|
||||
@ -510,6 +629,27 @@ async fn sync_models_from_upstream(
|
||||
}
|
||||
}
|
||||
|
||||
/// Deactivates models that were previously marked offline and are not in any active sync.
|
||||
/// These are manually added models that are no longer needed.
|
||||
async fn deactivate_orphaned_models(db: &AppDatabase) -> Result<i64, AppError> {
|
||||
use models::agents::model::Entity as MEntity;
|
||||
use models::agents::model::Column as MCol;
|
||||
|
||||
let now = Utc::now();
|
||||
let updated = MEntity::update_many()
|
||||
.set(models::agents::model::ActiveModel {
|
||||
status: Set(ModelStatus::Deprecated.to_string()),
|
||||
updated_at: Set(now),
|
||||
..Default::default()
|
||||
})
|
||||
.filter(MCol::Status.eq(ModelStatus::Offline.to_string()))
|
||||
.exec(db.writer())
|
||||
.await
|
||||
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
||||
|
||||
Ok(updated.rows_affected as i64)
|
||||
}
|
||||
|
||||
// HTTP helpers ---------------------------------------------------------------
|
||||
|
||||
/// List models from the upstream AI endpoint (`GET /v1/models`).
|
||||
|
||||
@ -280,7 +280,7 @@ impl AppService {
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to log issue open activity");
|
||||
tracing::warn!(error = ?e, "failed to log issue open activity");
|
||||
}
|
||||
|
||||
// Run AI triage asynchronously
|
||||
@ -363,7 +363,7 @@ impl AppService {
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to log issue update activity");
|
||||
tracing::warn!(error = ?e, "failed to log issue update activity");
|
||||
}
|
||||
|
||||
Ok(IssueResponse::from(model))
|
||||
@ -471,7 +471,7 @@ impl AppService {
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to log issue state change activity");
|
||||
tracing::warn!(error = ?e, "failed to log issue state change activity");
|
||||
}
|
||||
|
||||
Ok(IssueResponse::from(model))
|
||||
@ -566,7 +566,7 @@ impl AppService {
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to log issue delete activity");
|
||||
tracing::warn!(error = ?e, "failed to log issue delete activity");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -211,13 +211,13 @@ impl AppService {
|
||||
}
|
||||
|
||||
fn user_generate_access_key(&self) -> String {
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut rng = rand::rng();
|
||||
let mut access_key = String::with_capacity(68);
|
||||
access_key.push_str("gda_");
|
||||
for _ in 0..64 {
|
||||
access_key.push(chars[rng.gen_range(0..chars.len())] as char);
|
||||
access_key.push(chars[rng.random_range(0..chars.len())] as char);
|
||||
}
|
||||
access_key
|
||||
}
|
||||
|
||||
@ -168,7 +168,7 @@ impl AppService {
|
||||
plan: ws.plan,
|
||||
my_role: membership.role,
|
||||
created_at: ws.created_at,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user