All log calls in sync.rs now use slog macros: - sync_once: uses logger passed as parameter (sourced from AppService.logs) - sync_upstream_models (HTTP API): uses self.logs - Remove use of tracing::warn/info/error entirely The periodic background sync task and the HTTP API handler now write to the same slog logger as the rest of the application.
673 lines
22 KiB
Rust
673 lines
22 KiB
Rust
//! Synchronizes AI models from OpenRouter into the local database.
|
|
//!
|
|
//! Fetches the full model list via OpenRouter's `/api/v1/models` endpoint
|
|
//! (requires `OPENROUTER_API_KEY` in config or falls back to `AI_API_KEY`).
|
|
//!
|
|
//! OpenRouter returns rich metadata per model including `context_length`,
|
|
//! `pricing`, and `architecture.modality` — these are used to populate all
|
|
//! five model tables without any hard-coded heuristics.
|
|
//!
|
|
//! Usage: call `start_sync_task()` to launch a background task that syncs
|
|
//! immediately and then every 10 minutes. On app startup, run it once
|
|
//! eagerly before accepting traffic.
|
|
|
|
use std::time::Duration;
|
|
use tokio::time::interval;
|
|
use tokio::task::JoinHandle;
|
|
use slog::Logger;
|
|
|
|
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use chrono::Utc;
|
|
use db::database::AppDatabase;
|
|
use models::agents::model::Entity as ModelEntity;
|
|
use models::agents::model_capability::Entity as CapabilityEntity;
|
|
use models::agents::model_parameter_profile::Entity as ProfileEntity;
|
|
use models::agents::model_pricing::Entity as PricingEntity;
|
|
use models::agents::model_provider::Entity as ProviderEntity;
|
|
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 serde::Deserialize;
|
|
use serde::Serialize;
|
|
use session::Session;
|
|
use utoipa::ToSchema;
|
|
use uuid::Uuid;
|
|
|
|
// OpenRouter API types -------------------------------------------------------
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
struct OpenRouterResponse {
|
|
data: Vec<OpenRouterModel>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct OpenRouterModel {
|
|
id: String,
|
|
name: Option<String>,
|
|
#[serde(default)]
|
|
description: Option<String>,
|
|
pricing: Option<OpenRouterPricing>,
|
|
#[serde(default)]
|
|
context_length: Option<u64>,
|
|
#[serde(default)]
|
|
architecture: Option<OpenRouterArchitecture>,
|
|
#[serde(default)]
|
|
top_provider: Option<OpenRouterTopProvider>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct OpenRouterPricing {
|
|
prompt: String,
|
|
completion: String,
|
|
#[serde(default)]
|
|
request: Option<String>,
|
|
#[serde(default)]
|
|
image: Option<String>,
|
|
#[serde(default)]
|
|
input_cache_read: Option<String>,
|
|
#[serde(default)]
|
|
input_cache_write: Option<String>,
|
|
#[serde(default)]
|
|
web_search: Option<String>,
|
|
#[serde(default)]
|
|
internal_reasoning: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct OpenRouterArchitecture {
|
|
#[serde(default)]
|
|
modality: Option<String>,
|
|
#[serde(default)]
|
|
input_modalities: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
output_modalities: Option<Vec<String>>,
|
|
#[serde(default)]
|
|
tokenizer: Option<String>,
|
|
#[serde(default)]
|
|
instruct_type: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[allow(dead_code)]
|
|
struct OpenRouterTopProvider {
|
|
#[serde(default)]
|
|
context_length: Option<u64>,
|
|
#[serde(default)]
|
|
max_completion_tokens: Option<u64>,
|
|
#[serde(default)]
|
|
is_moderated: Option<bool>,
|
|
}
|
|
|
|
// Response type --------------------------------------------------------------
|
|
|
|
#[derive(Debug, Clone, Serialize, ToSchema)]
|
|
pub struct SyncModelsResponse {
|
|
pub models_created: i64,
|
|
pub models_updated: i64,
|
|
pub versions_created: i64,
|
|
pub pricing_created: i64,
|
|
pub capabilities_created: i64,
|
|
pub profiles_created: i64,
|
|
}
|
|
|
|
// Inference helpers (fallbacks when OpenRouter data is missing) ---------------
|
|
|
|
fn infer_modality(name: &str, arch_modality: Option<&str>) -> ModelModality {
|
|
if let Some(m) = arch_modality {
|
|
let m = m.to_lowercase();
|
|
if m.contains("text") || m.contains("chat") {
|
|
return ModelModality::Text;
|
|
}
|
|
if m.contains("image") || m.contains("vision") {
|
|
return ModelModality::Multimodal;
|
|
}
|
|
if m.contains("audio") || m.contains("speech") {
|
|
return ModelModality::Audio;
|
|
}
|
|
}
|
|
let lower = name.to_lowercase();
|
|
if lower.contains("vision")
|
|
|| lower.contains("dall-e")
|
|
|| lower.contains("gpt-image")
|
|
|| lower.contains("gpt-4o")
|
|
{
|
|
ModelModality::Multimodal
|
|
} else if lower.contains("embedding") {
|
|
ModelModality::Text
|
|
} else if lower.contains("whisper") || lower.contains("audio") {
|
|
ModelModality::Audio
|
|
} else {
|
|
ModelModality::Text
|
|
}
|
|
}
|
|
|
|
fn infer_capability(name: &str) -> ModelCapability {
|
|
let lower = name.to_lowercase();
|
|
if lower.contains("embedding") {
|
|
ModelCapability::Embedding
|
|
} else if lower.contains("code") {
|
|
ModelCapability::Code
|
|
} else {
|
|
ModelCapability::Chat
|
|
}
|
|
}
|
|
|
|
fn infer_context_length(ctx: Option<u64>) -> i64 {
|
|
ctx.map(|c| c as i64).unwrap_or(8_192)
|
|
}
|
|
|
|
fn infer_max_output(top_provider_max: Option<u64>) -> Option<i64> {
|
|
top_provider_max.map(|v| v as i64)
|
|
}
|
|
|
|
fn infer_capability_list(arch: &OpenRouterArchitecture) -> Vec<(CapabilityType, bool)> {
|
|
// Derive capabilities purely from OpenRouter architecture data.
|
|
// FunctionCall is a safe baseline for chat models.
|
|
let mut caps = vec![(CapabilityType::FunctionCall, true)];
|
|
|
|
// Vision capability from modality.
|
|
if let Some(m) = &arch.modality {
|
|
let m = m.to_lowercase();
|
|
if m.contains("image") || m.contains("vision") {
|
|
caps.push((CapabilityType::Vision, true));
|
|
}
|
|
if m.contains("text") || m.contains("chat") {
|
|
caps.push((CapabilityType::ToolUse, true));
|
|
}
|
|
}
|
|
caps
|
|
}
|
|
|
|
// Provider helpers -----------------------------------------------------------
|
|
|
|
/// Extract provider slug from OpenRouter model ID (e.g. "anthropic/claude-3.5-sonnet" → "anthropic").
|
|
fn extract_provider(model_id: &str) -> &str {
|
|
model_id.split('/').next().unwrap_or("unknown")
|
|
}
|
|
|
|
/// Normalize a provider slug to a short canonical name.
|
|
fn normalize_provider_name(slug: &str) -> &'static str {
|
|
match slug {
|
|
"openai" => "openai",
|
|
"anthropic" => "anthropic",
|
|
"google" | "google-ai" => "google",
|
|
"mistralai" => "mistral",
|
|
"meta-llama" | "meta" => "meta",
|
|
"deepseek" => "deepseek",
|
|
"azure" | "azure-openai" => "azure",
|
|
"x-ai" | "xai" => "xai",
|
|
s => Box::leak(s.to_string().into_boxed_str()),
|
|
}
|
|
}
|
|
|
|
fn provider_display_name(name: &str) -> String {
|
|
match name {
|
|
"openai" => "OpenAI".to_string(),
|
|
"anthropic" => "Anthropic".to_string(),
|
|
"google" => "Google DeepMind".to_string(),
|
|
"mistral" => "Mistral AI".to_string(),
|
|
"meta" => "Meta".to_string(),
|
|
"deepseek" => "DeepSeek".to_string(),
|
|
"azure" => "Microsoft Azure".to_string(),
|
|
"xai" => "xAI".to_string(),
|
|
s => s.to_string(),
|
|
}
|
|
}
|
|
|
|
// Upsert helpers -------------------------------------------------------------
|
|
|
|
async fn upsert_provider(
|
|
db: &AppDatabase,
|
|
slug: &str,
|
|
) -> Result<ProviderModel, AppError> {
|
|
let name = normalize_provider_name(slug);
|
|
let display = provider_display_name(name);
|
|
let now = Utc::now();
|
|
|
|
use models::agents::model_provider::Column as PCol;
|
|
if let Some(existing) = ProviderEntity::find()
|
|
.filter(PCol::Name.eq(name))
|
|
.one(db)
|
|
.await?
|
|
{
|
|
let mut active: models::agents::model_provider::ActiveModel = existing.into();
|
|
active.updated_at = Set(now);
|
|
active.update(db).await?;
|
|
Ok(ProviderEntity::find()
|
|
.filter(PCol::Name.eq(name))
|
|
.one(db)
|
|
.await?
|
|
.unwrap())
|
|
} else {
|
|
let active = models::agents::model_provider::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
name: Set(name.to_string()),
|
|
display_name: Set(display.to_string()),
|
|
website: Set(None),
|
|
status: Set(ModelStatus::Active.to_string()),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
active.insert(db).await.map_err(AppError::from)
|
|
}
|
|
}
|
|
|
|
/// Upsert a model record and return (model, is_new).
|
|
async fn upsert_model(
|
|
db: &AppDatabase,
|
|
provider_id: Uuid,
|
|
model_id_str: &str,
|
|
or_model: &OpenRouterModel,
|
|
) -> Result<(models::agents::model::Model, bool), AppError> {
|
|
let now = Utc::now();
|
|
let modality_str = or_model
|
|
.architecture
|
|
.as_ref()
|
|
.and_then(|a| a.modality.as_deref());
|
|
let modality = infer_modality(model_id_str, modality_str);
|
|
let capability = infer_capability(model_id_str);
|
|
|
|
// OpenRouter context_length takes priority; fall back to inference
|
|
let context_length = infer_context_length(or_model.context_length);
|
|
|
|
let max_output =
|
|
infer_max_output(or_model.top_provider.as_ref().and_then(|p| p.max_completion_tokens));
|
|
|
|
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_str))
|
|
.one(db)
|
|
.await?
|
|
{
|
|
let mut active: models::agents::model::ActiveModel = existing.clone().into();
|
|
active.context_length = Set(context_length);
|
|
active.max_output_tokens = Set(max_output);
|
|
active.status = Set(ModelStatus::Active.to_string());
|
|
active.updated_at = Set(now);
|
|
active.update(db).await?;
|
|
Ok((ModelEntity::find_by_id(existing.id).one(db).await?.unwrap(), false))
|
|
} else {
|
|
let active = models::agents::model::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
provider_id: Set(provider_id),
|
|
name: Set(model_id_str.to_string()),
|
|
modality: Set(modality.to_string()),
|
|
capability: Set(capability.to_string()),
|
|
context_length: Set(context_length),
|
|
max_output_tokens: Set(max_output),
|
|
training_cutoff: Set(None),
|
|
is_open_source: Set(false),
|
|
status: Set(ModelStatus::Active.to_string()),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
..Default::default()
|
|
};
|
|
let inserted = active.insert(db).await.map_err(AppError::from)?;
|
|
Ok((inserted, true))
|
|
}
|
|
}
|
|
|
|
/// Upsert default version for a model.
|
|
async fn upsert_version(
|
|
db: &AppDatabase,
|
|
model_uuid: Uuid,
|
|
) -> Result<(models::agents::model_version::Model, bool), AppError> {
|
|
use models::agents::model_version::Column as VCol;
|
|
let now = Utc::now();
|
|
if let Some(existing) = VersionEntity::find()
|
|
.filter(VCol::ModelId.eq(model_uuid))
|
|
.filter(VCol::IsDefault.eq(true))
|
|
.one(db)
|
|
.await?
|
|
{
|
|
Ok((existing, false))
|
|
} else {
|
|
let active = models::agents::model_version::ActiveModel {
|
|
id: Set(Uuid::now_v7()),
|
|
model_id: Set(model_uuid),
|
|
version: Set("1".to_string()),
|
|
release_date: Set(None),
|
|
change_log: Set(None),
|
|
is_default: Set(true),
|
|
status: Set(ModelStatus::Active.to_string()),
|
|
created_at: Set(now),
|
|
};
|
|
let inserted = active.insert(db).await.map_err(AppError::from)?;
|
|
Ok((inserted, true))
|
|
}
|
|
}
|
|
|
|
/// Upsert pricing for a model version. Returns true if created.
|
|
async fn upsert_pricing(
|
|
db: &AppDatabase,
|
|
version_uuid: Uuid,
|
|
pricing: Option<&OpenRouterPricing>,
|
|
) -> Result<bool, AppError> {
|
|
use models::agents::model_pricing::Column as PCol;
|
|
let existing = PricingEntity::find()
|
|
.filter(PCol::ModelVersionId.eq(version_uuid))
|
|
.one(db)
|
|
.await?;
|
|
if existing.is_some() {
|
|
return Ok(false);
|
|
}
|
|
|
|
// OpenRouter prices are per-million-tokens strings; if missing, insert zero prices.
|
|
let (input_str, output_str) = if let Some(p) = pricing {
|
|
(p.prompt.clone(), p.completion.clone())
|
|
} else {
|
|
("0.00".to_string(), "0.00".to_string())
|
|
};
|
|
|
|
let active = models::agents::model_pricing::ActiveModel {
|
|
id: Set(Uuid::now_v7().as_u128() as i64),
|
|
model_version_id: Set(version_uuid),
|
|
input_price_per_1k_tokens: Set(input_str),
|
|
output_price_per_1k_tokens: Set(output_str),
|
|
currency: Set("USD".to_string()),
|
|
effective_from: Set(Utc::now()),
|
|
};
|
|
active.insert(db).await.map_err(AppError::from)?;
|
|
Ok(true)
|
|
}
|
|
|
|
/// Upsert capability records for a model version. Returns count of new records.
|
|
async fn upsert_capabilities(
|
|
db: &AppDatabase,
|
|
version_uuid: Uuid,
|
|
arch: Option<&OpenRouterArchitecture>,
|
|
) -> Result<i64, AppError> {
|
|
use models::agents::model_capability::Column as CCol;
|
|
let caps = infer_capability_list(arch.unwrap_or(&OpenRouterArchitecture {
|
|
modality: None,
|
|
input_modalities: None,
|
|
output_modalities: None,
|
|
tokenizer: None,
|
|
instruct_type: None,
|
|
}));
|
|
let now = Utc::now();
|
|
let mut created = 0i64;
|
|
|
|
for (cap_type, supported) in caps {
|
|
let exists = CapabilityEntity::find()
|
|
.filter(CCol::ModelVersionId.eq(version_uuid))
|
|
.filter(CCol::Capability.eq(cap_type.to_string()))
|
|
.one(db)
|
|
.await?;
|
|
if exists.is_some() {
|
|
continue;
|
|
}
|
|
let active = models::agents::model_capability::ActiveModel {
|
|
id: Set(Uuid::now_v7().as_u128() as i64),
|
|
model_version_id: Set(version_uuid.as_u128() as i64),
|
|
capability: Set(cap_type.to_string()),
|
|
is_supported: Set(supported),
|
|
created_at: Set(now),
|
|
};
|
|
active.insert(db).await.map_err(AppError::from)?;
|
|
created += 1;
|
|
}
|
|
Ok(created)
|
|
}
|
|
|
|
/// Upsert default parameter profile for a model version. Returns true if created.
|
|
async fn upsert_parameter_profile(
|
|
db: &AppDatabase,
|
|
version_uuid: Uuid,
|
|
model_name: &str,
|
|
) -> Result<bool, AppError> {
|
|
use models::agents::model_parameter_profile::Column as PCol;
|
|
let existing = ProfileEntity::find()
|
|
.filter(PCol::ModelVersionId.eq(version_uuid))
|
|
.one(db)
|
|
.await?;
|
|
if existing.is_some() {
|
|
return Ok(false);
|
|
}
|
|
|
|
let lower = model_name.to_lowercase();
|
|
let (t_min, t_max) = if lower.contains("o1") || lower.contains("o3") {
|
|
(1.0, 1.0)
|
|
} else {
|
|
(0.0, 2.0)
|
|
};
|
|
|
|
let active = models::agents::model_parameter_profile::ActiveModel {
|
|
id: Set(Uuid::now_v7().as_u128() as i64),
|
|
model_version_id: Set(version_uuid),
|
|
temperature_min: Set(t_min),
|
|
temperature_max: Set(t_max),
|
|
top_p_min: Set(0.0),
|
|
top_p_max: Set(1.0),
|
|
frequency_penalty_supported: Set(true),
|
|
presence_penalty_supported: Set(true),
|
|
};
|
|
active.insert(db).await.map_err(AppError::from)?;
|
|
Ok(true)
|
|
}
|
|
|
|
impl AppService {
|
|
/// Sync models from OpenRouter into the local database.
|
|
///
|
|
/// Calls OpenRouter's public `GET /api/v1/models` endpoint (no auth required),
|
|
/// then upserts provider / model / version / pricing / capability /
|
|
/// parameter-profile records.
|
|
///
|
|
/// OpenRouter returns `context_length`, `pricing`, and `architecture.modality`
|
|
/// per model — these drive all field population. No model names are hardcoded.
|
|
pub async fn sync_upstream_models(
|
|
&self,
|
|
_ctx: &Session,
|
|
) -> Result<SyncModelsResponse, AppError> {
|
|
let client = reqwest::Client::new();
|
|
let resp: OpenRouterResponse = client
|
|
.get("https://openrouter.ai/api/v1/models")
|
|
.send()
|
|
.await
|
|
.map_err(|e| AppError::InternalServerError(format!("OpenRouter API request failed: {}", e)))?
|
|
.error_for_status()
|
|
.map_err(|e| AppError::InternalServerError(format!("OpenRouter API error: {}", e)))?
|
|
.json()
|
|
.await
|
|
.map_err(|e| AppError::InternalServerError(format!("Failed to parse OpenRouter response: {}", e)))?;
|
|
|
|
let mut models_created = 0i64;
|
|
let mut models_updated = 0i64;
|
|
let mut versions_created = 0i64;
|
|
let mut pricing_created = 0i64;
|
|
let mut capabilities_created = 0i64;
|
|
let mut profiles_created = 0i64;
|
|
|
|
for or_model in resp.data {
|
|
// Filter out openrouter/auto which has negative pricing
|
|
if or_model.id == "openrouter/auto" {
|
|
continue;
|
|
}
|
|
|
|
let provider_slug = extract_provider(&or_model.id);
|
|
let provider = upsert_provider(&self.db, provider_slug).await?;
|
|
|
|
let (model_record, is_new) =
|
|
upsert_model(&self.db, provider.id, &or_model.id, &or_model).await?;
|
|
|
|
if is_new {
|
|
models_created += 1;
|
|
} else {
|
|
models_updated += 1;
|
|
}
|
|
|
|
let (version_record, version_is_new) =
|
|
match upsert_version(&self.db, model_record.id).await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
slog::warn!(self.logs, "{}", format!("sync_upstream_models: upsert_version error: {:?}", e));
|
|
continue;
|
|
}
|
|
};
|
|
if version_is_new {
|
|
versions_created += 1;
|
|
}
|
|
|
|
if let Err(e) = upsert_pricing(&self.db, version_record.id, or_model.pricing.as_ref()).await {
|
|
slog::warn!(self.logs, "{}", format!("sync_upstream_models: upsert_pricing error: {:?}", e));
|
|
} else {
|
|
pricing_created += 1;
|
|
}
|
|
|
|
capabilities_created +=
|
|
upsert_capabilities(&self.db, version_record.id, or_model.architecture.as_ref())
|
|
.await
|
|
.unwrap_or(0);
|
|
|
|
if upsert_parameter_profile(&self.db, version_record.id, &or_model.id).await.unwrap_or(false) {
|
|
profiles_created += 1;
|
|
}
|
|
}
|
|
|
|
Ok(SyncModelsResponse {
|
|
models_created,
|
|
models_updated,
|
|
versions_created,
|
|
pricing_created,
|
|
capabilities_created,
|
|
profiles_created,
|
|
})
|
|
}
|
|
|
|
/// Spawn a background task that syncs OpenRouter models immediately
|
|
/// and then every 10 minutes. Returns the `JoinHandle`.
|
|
///
|
|
/// Failures are logged but do not stop the task — it keeps retrying.
|
|
pub fn start_sync_task(self) -> JoinHandle<()> {
|
|
let db = self.db.clone();
|
|
let log = self.logs.clone();
|
|
|
|
tokio::spawn(async move {
|
|
// Run once immediately on startup before taking traffic.
|
|
Self::sync_once(&db, &log).await;
|
|
|
|
let mut tick = interval(Duration::from_secs(60 * 10));
|
|
loop {
|
|
tick.tick().await;
|
|
Self::sync_once(&db, &log).await;
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Perform a single sync pass. Errors are logged and silently swallowed
|
|
/// so the periodic task never stops.
|
|
async fn sync_once(db: &AppDatabase, log: &Logger) {
|
|
let client = reqwest::Client::new();
|
|
let resp = match client
|
|
.get("https://openrouter.ai/api/v1/models")
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(r) => match r.error_for_status() {
|
|
Ok(resp) => match resp.json::<OpenRouterResponse>().await {
|
|
Ok(resp) => resp,
|
|
Err(e) => {
|
|
slog::error!(log, "{}", format!("OpenRouter model sync: failed to parse response: {}", e));
|
|
return;
|
|
}
|
|
},
|
|
Err(e) => {
|
|
slog::error!(log, "{}", format!("OpenRouter model sync: API error: {}", e));
|
|
return;
|
|
}
|
|
},
|
|
Err(e) => {
|
|
slog::error!(log, "{}", format!("OpenRouter model sync: request failed: {}", e));
|
|
return;
|
|
}
|
|
};
|
|
|
|
let mut models_created = 0i64;
|
|
let mut models_updated = 0i64;
|
|
let mut versions_created = 0i64;
|
|
let mut pricing_created = 0i64;
|
|
let mut capabilities_created = 0i64;
|
|
let mut profiles_created = 0i64;
|
|
|
|
for or_model in resp.data {
|
|
if or_model.id == "openrouter/auto" {
|
|
continue;
|
|
}
|
|
|
|
let provider_slug = extract_provider(&or_model.id);
|
|
let provider = match upsert_provider(db, provider_slug).await {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
slog::warn!(log, "{}", format!("OpenRouter model sync: upsert_provider error: {:?}", e));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let model_record = match upsert_model(db, provider.id, &or_model.id, &or_model).await {
|
|
Ok((m, true)) => {
|
|
models_created += 1;
|
|
m
|
|
}
|
|
Ok((m, false)) => {
|
|
models_updated += 1;
|
|
m
|
|
}
|
|
Err(e) => {
|
|
slog::warn!(log, "{}", format!("OpenRouter model sync: upsert_model error: {:?}", e));
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let (version_record, version_is_new) = match upsert_version(db, model_record.id).await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
slog::warn!(log, "{}", format!("OpenRouter model sync: upsert_version error: {:?}", e));
|
|
continue;
|
|
}
|
|
};
|
|
if version_is_new {
|
|
versions_created += 1;
|
|
}
|
|
|
|
if upsert_pricing(db, version_record.id, or_model.pricing.as_ref())
|
|
.await
|
|
.unwrap_or(false)
|
|
{
|
|
pricing_created += 1;
|
|
}
|
|
|
|
capabilities_created +=
|
|
upsert_capabilities(db, version_record.id, or_model.architecture.as_ref())
|
|
.await
|
|
.unwrap_or(0);
|
|
|
|
if upsert_parameter_profile(db, version_record.id, &or_model.id)
|
|
.await
|
|
.unwrap_or(false)
|
|
{
|
|
profiles_created += 1;
|
|
}
|
|
}
|
|
|
|
slog::info!(log, "{}",
|
|
format!(
|
|
"OpenRouter model sync complete: created={} updated={} \
|
|
versions={} pricing={} capabilities={} profiles={}",
|
|
models_created,
|
|
models_updated,
|
|
versions_created,
|
|
pricing_created,
|
|
capabilities_created,
|
|
profiles_created
|
|
)
|
|
);
|
|
}
|
|
}
|