feat(service): auto-sync OpenRouter models on app startup and every 10 minutes
- Add `start_sync_task()` in agent/sync.rs: spawns a background task that syncs immediately on app startup, then every 10 minutes. - `sync_once()` performs a single pass; errors are logged and swallowed so the periodic task never stops. - Remove authentication requirement from OpenRouter API (no API key needed). - Call `service.start_sync_task()` from main.rs after AppService init. - Also update the existing `sync_upstream_models` (HTTP API) to remove the now-unnecessary API key requirement for consistency.
This commit is contained in:
parent
329b526bfb
commit
9368df54da
@ -107,6 +107,10 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
let service = AppService::new(cfg.clone()).await?;
|
let service = AppService::new(cfg.clone()).await?;
|
||||||
slog::info!(log, "AppService initialized");
|
slog::info!(log, "AppService initialized");
|
||||||
|
|
||||||
|
// Spawn background task: sync OpenRouter models immediately on startup,
|
||||||
|
// then every 10 minutes.
|
||||||
|
let _model_sync_handle = service.clone().start_sync_task();
|
||||||
|
|
||||||
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
|
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
|
||||||
let worker_service = service.clone();
|
let worker_service = service.clone();
|
||||||
let log_for_http = log.clone();
|
let log_for_http = log.clone();
|
||||||
|
|||||||
@ -6,6 +6,14 @@
|
|||||||
//! OpenRouter returns rich metadata per model including `context_length`,
|
//! OpenRouter returns rich metadata per model including `context_length`,
|
||||||
//! `pricing`, and `architecture.modality` — these are used to populate all
|
//! `pricing`, and `architecture.modality` — these are used to populate all
|
||||||
//! five model tables without any hard-coded heuristics.
|
//! 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 crate::AppService;
|
use crate::AppService;
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
@ -546,9 +554,9 @@ async fn upsert_parameter_profile(
|
|||||||
impl AppService {
|
impl AppService {
|
||||||
/// Sync models from OpenRouter into the local database.
|
/// Sync models from OpenRouter into the local database.
|
||||||
///
|
///
|
||||||
/// Calls OpenRouter's `GET /api/v1/models` using `OPENROUTER_API_KEY`
|
/// Calls OpenRouter's public `GET /api/v1/models` endpoint (no auth required),
|
||||||
/// (falls back to `AI_API_KEY` if not set), then upserts provider /
|
/// then upserts provider / model / version / pricing / capability /
|
||||||
/// model / version / pricing / capability / parameter-profile records.
|
/// parameter-profile records.
|
||||||
///
|
///
|
||||||
/// OpenRouter returns `context_length`, `pricing`, and `architecture.modality`
|
/// OpenRouter returns `context_length`, `pricing`, and `architecture.modality`
|
||||||
/// per model — these drive all inference-free field population.
|
/// per model — these drive all inference-free field population.
|
||||||
@ -557,20 +565,9 @@ impl AppService {
|
|||||||
&self,
|
&self,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
) -> Result<SyncModelsResponse, AppError> {
|
) -> Result<SyncModelsResponse, AppError> {
|
||||||
// Resolve API key: prefer OPENROUTER_API_KEY env var, fall back to AI_API_KEY.
|
|
||||||
let api_key = std::env::var("OPENROUTER_API_KEY")
|
|
||||||
.ok()
|
|
||||||
.or_else(|| self.config.ai_api_key().ok())
|
|
||||||
.ok_or_else(|| {
|
|
||||||
AppError::InternalServerError(
|
|
||||||
"OPENROUTER_API_KEY or AI_API_KEY must be configured to sync models".into(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
let resp: OpenRouterResponse = client
|
let resp: OpenRouterResponse = client
|
||||||
.get("https://openrouter.ai/api/v1/models")
|
.get("https://openrouter.ai/api/v1/models")
|
||||||
.header("Authorization", format!("Bearer {api_key}"))
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::InternalServerError(format!("OpenRouter API request failed: {}", e)))?
|
.map_err(|e| AppError::InternalServerError(format!("OpenRouter API request failed: {}", e)))?
|
||||||
@ -639,4 +636,130 @@ impl AppService {
|
|||||||
profiles_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();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Run once immediately on startup before taking traffic.
|
||||||
|
Self::sync_once(&db).await;
|
||||||
|
|
||||||
|
let mut tick = interval(Duration::from_secs(60 * 10));
|
||||||
|
loop {
|
||||||
|
tick.tick().await;
|
||||||
|
Self::sync_once(&db).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a single sync pass. Errors are logged and silently swallowed
|
||||||
|
/// so the periodic task never stops.
|
||||||
|
async fn sync_once(db: &AppDatabase) {
|
||||||
|
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) => {
|
||||||
|
tracing::error!("OpenRouter model sync: failed to parse response: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("OpenRouter model sync: API error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("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) => {
|
||||||
|
tracing::warn!("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) => {
|
||||||
|
tracing::warn!("OpenRouter model sync: upsert_model error: {:?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match upsert_version(db, model_record.id).await {
|
||||||
|
Ok((version_record, version_is_new)) => {
|
||||||
|
if version_is_new {
|
||||||
|
versions_created += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if upsert_pricing(db, version_record.id, or_model.pricing.as_ref(), &or_model.id)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
pricing_created += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
capabilities_created +=
|
||||||
|
upsert_capabilities(db, version_record.id, &or_model.id)
|
||||||
|
.await
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if upsert_parameter_profile(db, version_record.id, &or_model.id)
|
||||||
|
.await
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
profiles_created += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("OpenRouter model sync: upsert_version error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"OpenRouter model sync complete: created={} updated={} \
|
||||||
|
versions={} pricing={} capabilities={} profiles={}",
|
||||||
|
models_created,
|
||||||
|
models_updated,
|
||||||
|
versions_created,
|
||||||
|
pricing_created,
|
||||||
|
capabilities_created,
|
||||||
|
profiles_created
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user