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::model_version::Entity as VersionEntity;
|
||||||
use models::agents::{CapabilityType, ModelCapability, ModelModality, ModelStatus};
|
use models::agents::{CapabilityType, ModelCapability, ModelModality, ModelStatus};
|
||||||
use sea_orm::prelude::*;
|
use sea_orm::prelude::*;
|
||||||
use sea_orm::Set;
|
use sea_orm::{QueryOrder, Set};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use session::Session;
|
use session::Session;
|
||||||
@ -94,6 +94,8 @@ struct UpstreamPricing {
|
|||||||
pub struct SyncModelsResponse {
|
pub struct SyncModelsResponse {
|
||||||
pub models_created: i64,
|
pub models_created: i64,
|
||||||
pub models_updated: i64,
|
pub models_updated: i64,
|
||||||
|
pub models_offline: i64,
|
||||||
|
pub models_deactivated: i64,
|
||||||
pub versions_created: i64,
|
pub versions_created: i64,
|
||||||
pub pricing_created: i64,
|
pub pricing_created: i64,
|
||||||
pub capabilities_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();
|
let mut active: models::agents::model_provider::ActiveModel = existing.into();
|
||||||
active.updated_at = Set(now);
|
active.updated_at = Set(now);
|
||||||
active.update(db).await
|
active.update(db).await.map_err(AppError::from)
|
||||||
} else {
|
} else {
|
||||||
let active = models::agents::model_provider::ActiveModel {
|
let active = models::agents::model_provider::ActiveModel {
|
||||||
id: Set(Uuid::now_v7()),
|
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,
|
db: &AppDatabase,
|
||||||
provider_id: Uuid,
|
provider_id: Uuid,
|
||||||
model: &UpstreamModel,
|
model: &UpstreamModel,
|
||||||
@ -248,15 +253,18 @@ async fn upsert_model(
|
|||||||
let capability = infer_capability(model);
|
let capability = infer_capability(model);
|
||||||
let ctx = context_length(model);
|
let ctx = context_length(model);
|
||||||
let max_out = max_output_tokens(model);
|
let max_out = max_output_tokens(model);
|
||||||
|
let model_name = extract_model_name(model);
|
||||||
|
|
||||||
use models::agents::model::Column as MCol;
|
use models::agents::model::Column as MCol;
|
||||||
if let Some(existing) = ModelEntity::find()
|
if let Some(existing) = ModelEntity::find()
|
||||||
.filter(MCol::ProviderId.eq(provider_id))
|
.filter(MCol::Name.eq(&model_name))
|
||||||
.filter(MCol::Name.eq(&model.id))
|
|
||||||
.one(db)
|
.one(db)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
// Update existing model (deduplicated by name)
|
||||||
let mut active: models::agents::model::ActiveModel = existing.clone().into();
|
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.context_length = Set(ctx);
|
||||||
active.max_output_tokens = Set(max_out);
|
active.max_output_tokens = Set(max_out);
|
||||||
active.status = Set(ModelStatus::Active.to_string());
|
active.status = Set(ModelStatus::Active.to_string());
|
||||||
@ -264,10 +272,11 @@ async fn upsert_model(
|
|||||||
let updated = active.update(db).await?;
|
let updated = active.update(db).await?;
|
||||||
Ok((updated, false))
|
Ok((updated, false))
|
||||||
} else {
|
} else {
|
||||||
|
// Create new model
|
||||||
let active = models::agents::model::ActiveModel {
|
let active = models::agents::model::ActiveModel {
|
||||||
id: Set(Uuid::now_v7()),
|
id: Set(Uuid::now_v7()),
|
||||||
provider_id: Set(provider_id),
|
provider_id: Set(provider_id),
|
||||||
name: Set(model.id.clone()),
|
name: Set(model_name),
|
||||||
modality: Set(modality.to_string()),
|
modality: Set(modality.to_string()),
|
||||||
capability: Set(capability.to_string()),
|
capability: Set(capability.to_string()),
|
||||||
context_length: Set(ctx),
|
context_length: Set(ctx),
|
||||||
@ -422,20 +431,123 @@ async fn upsert_parameter_profile(
|
|||||||
|
|
||||||
// Core sync logic ------------------------------------------------------------
|
// 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(
|
async fn sync_models_from_upstream(
|
||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
models: Vec<UpstreamModel>,
|
upstream_models: Vec<UpstreamModel>,
|
||||||
) -> SyncModelsResponse {
|
) -> 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_created = 0i64;
|
||||||
let mut models_updated = 0i64;
|
let mut models_updated = 0i64;
|
||||||
|
let models_deactivated: i64;
|
||||||
let mut versions_created = 0i64;
|
let mut versions_created = 0i64;
|
||||||
let mut pricing_created = 0i64;
|
let mut pricing_created = 0i64;
|
||||||
let mut capabilities_created = 0i64;
|
let mut capabilities_created = 0i64;
|
||||||
let mut profiles_created = 0i64;
|
let mut profiles_created = 0i64;
|
||||||
|
|
||||||
for model in models {
|
for model in deduplicated_models {
|
||||||
let provider_slug = extract_provider_name(&model);
|
let provider_slug = extract_provider_name(model);
|
||||||
let provider = match upsert_provider(db, provider_slug).await {
|
let provider = match upsert_provider(db, &provider_slug).await {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
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 {
|
let (model_record, _is_new) = match upsert_model_by_name(db, provider.id, model).await {
|
||||||
Ok((m, n)) => {
|
Ok((m, created)) => {
|
||||||
if n {
|
if created {
|
||||||
models_created += 1;
|
models_created += 1;
|
||||||
} else {
|
} else {
|
||||||
models_updated += 1;
|
models_updated += 1;
|
||||||
}
|
}
|
||||||
(m, n)
|
(m, created)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@ -488,11 +600,11 @@ async fn sync_models_from_upstream(
|
|||||||
pricing_created += 1;
|
pricing_created += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
capabilities_created += upsert_capabilities(db, version_record.id, &model)
|
capabilities_created += upsert_capabilities(db, version_record.id, model)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
if upsert_parameter_profile(db, version_record.id, &model)
|
if upsert_parameter_profile(db, version_record.id, model)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(false)
|
.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 {
|
SyncModelsResponse {
|
||||||
models_created,
|
models_created,
|
||||||
models_updated,
|
models_updated,
|
||||||
|
models_offline,
|
||||||
|
models_deactivated,
|
||||||
versions_created,
|
versions_created,
|
||||||
pricing_created,
|
pricing_created,
|
||||||
capabilities_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 ---------------------------------------------------------------
|
// HTTP helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
/// List models from the upstream AI endpoint (`GET /v1/models`).
|
/// List models from the upstream AI endpoint (`GET /v1/models`).
|
||||||
|
|||||||
@ -280,7 +280,7 @@ impl AppService {
|
|||||||
)
|
)
|
||||||
.await
|
.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
|
// Run AI triage asynchronously
|
||||||
@ -363,7 +363,7 @@ impl AppService {
|
|||||||
)
|
)
|
||||||
.await
|
.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))
|
Ok(IssueResponse::from(model))
|
||||||
@ -471,7 +471,7 @@ impl AppService {
|
|||||||
)
|
)
|
||||||
.await
|
.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))
|
Ok(IssueResponse::from(model))
|
||||||
@ -566,7 +566,7 @@ impl AppService {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
tracing::warn!(error = %e, "failed to log issue delete activity");
|
tracing::warn!(error = ?e, "failed to log issue delete activity");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@ -211,13 +211,13 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn user_generate_access_key(&self) -> String {
|
fn user_generate_access_key(&self) -> String {
|
||||||
use rand::Rng;
|
use rand::RngExt;
|
||||||
let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::rng();
|
||||||
let mut access_key = String::with_capacity(68);
|
let mut access_key = String::with_capacity(68);
|
||||||
access_key.push_str("gda_");
|
access_key.push_str("gda_");
|
||||||
for _ in 0..64 {
|
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
|
access_key
|
||||||
}
|
}
|
||||||
|
|||||||
@ -168,7 +168,7 @@ impl AppService {
|
|||||||
plan: ws.plan,
|
plan: ws.plan,
|
||||||
my_role: membership.role,
|
my_role: membership.role,
|
||||||
created_at: ws.created_at,
|
created_at: ws.created_at,
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user