feat(agent/sync): sync non-OpenRouter models from upstream endpoint
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions

When upstream /v1/models returns models not yet in OpenRouter's catalog
(e.g. brand-new models like DeepSeek-V4), also upsert them through the
same pipeline (provider → model → version → pricing → capabilities →
parameter_profile) with inferred defaults, instead of silently dropping
them. Previously the direct-sync fallback only triggered when *zero*
OpenRouter matches existed.
This commit is contained in:
ZhenYi 2026-04-26 15:17:33 +08:00
parent 27cd4ea83c
commit 638dfd7a6e

View File

@ -5,7 +5,10 @@
//! 2. Fetch full metadata (pricing, context_length, capabilities) for those //! 2. Fetch full metadata (pricing, context_length, capabilities) for those
//! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth). //! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth).
//! 3. Upsert provider / model / version / pricing / capability / profile //! 3. Upsert provider / model / version / pricing / capability / profile
//! records only for models the client can actually call. //! records for models the client can actually call.
//! 4. Models accessible from the user's endpoint but NOT in OpenRouter's
//! catalog ("stranger" models) are also upserted through the same
//! pipeline with inferred defaults (direct sync).
//! //!
//! Usage: call `start_sync_task()` to launch a background task that syncs //! Usage: call `start_sync_task()` to launch a background task that syncs
//! immediately and then every 10 minutes. On app startup, run it once //! immediately and then every 10 minutes. On app startup, run it once
@ -704,7 +707,11 @@ impl AppService {
/// Steps: /// Steps:
/// 1. Call `client.models().list()` to get the set of accessible model IDs. /// 1. Call `client.models().list()` to get the set of accessible model IDs.
/// 2. Fetch full model list from OpenRouter's public `/api/v1/models` endpoint. /// 2. Fetch full model list from OpenRouter's public `/api/v1/models` endpoint.
/// 3. Keep only models whose ID appears in the accessible set, then upsert. /// 3. Keep only models whose ID appears in the accessible set, then upsert
/// with full OpenRouter metadata.
/// 4. Models NOT in OpenRouter's catalog are also upserted through the
/// same pipeline (provider → model → version → pricing → capabilities
/// → parameter_profile) with inferred defaults.
pub async fn sync_upstream_models( pub async fn sync_upstream_models(
&self, &self,
_ctx: &Session, _ctx: &Session,
@ -733,17 +740,21 @@ impl AppService {
.filter(|m| m.id != "openrouter/auto") .filter(|m| m.id != "openrouter/auto")
.collect(); .collect();
let filtered_count = filtered.len(); // Identify "stranger" models: accessible from the user's endpoint but
// NOT present in OpenRouter's public catalog. These are also upserted
// through the same pipeline (provider → model → version → pricing →
// capabilities → parameter_profile) with inferred defaults.
let or_matched_ids: std::collections::HashSet<&str> = filtered
.iter()
.map(|m| m.id.as_str())
.collect();
let unknown_ids: Vec<&str> = available_ids
.iter()
.filter(|id| !or_matched_ids.contains(id.as_str()))
.map(|s| s.as_str())
.collect();
// Fallback: if no OpenRouter metadata matches, sync models directly from let filtered_count = filtered.len();
// the user's endpoint (handles Bailian/MiniMax and other non-OpenRouter providers).
if filtered_count == 0 && !available_ids.is_empty() {
tracing::info!(
model_count = available_ids.len(),
"sync_upstream_models: no OpenRouter matches, falling back to direct sync"
);
return Ok(sync_models_direct(&self.db, &available_ids).await);
}
let mut models_created = 0i64; let mut models_created = 0i64;
let mut models_updated = 0i64; let mut models_updated = 0i64;
@ -752,6 +763,37 @@ impl AppService {
let mut capabilities_created = 0i64; let mut capabilities_created = 0i64;
let mut profiles_created = 0i64; let mut profiles_created = 0i64;
// Sync stranger models (non-OpenRouter) through the direct pipeline.
if !unknown_ids.is_empty() {
tracing::info!(
unknown_count = unknown_ids.len(),
"sync_upstream_models: {} models not in OpenRouter catalog, syncing directly",
unknown_ids.len()
);
let unknown_set: std::collections::HashSet<String> =
unknown_ids.iter().map(|s| ToString::to_string(s)).collect();
let direct_result = sync_models_direct(&self.db, &unknown_set).await;
models_created += direct_result.models_created;
models_updated += direct_result.models_updated;
versions_created += direct_result.versions_created;
pricing_created += direct_result.pricing_created;
capabilities_created += direct_result.capabilities_created;
profiles_created += direct_result.profiles_created;
}
// If no OpenRouter metadata matched at all, the direct sync above
// already handled everything — return early.
if filtered_count == 0 {
return Ok(SyncModelsResponse {
models_created,
models_updated,
versions_created,
pricing_created,
capabilities_created,
profiles_created,
});
}
for or_model in filtered { for or_model in filtered {
let provider_slug = extract_provider(&or_model.id); let provider_slug = extract_provider(&or_model.id);
let provider = match upsert_provider(&self.db, provider_slug).await { let provider = match upsert_provider(&self.db, provider_slug).await {
@ -823,8 +865,10 @@ impl AppService {
filtered_count, filtered_count,
models_created, models_created,
models_updated, models_updated,
"sync_upstream_models: synced {} accessible models", "sync_upstream_models: synced {} accessible models ({}) OpenRouter + ({}) direct",
filtered_count filtered_count + unknown_ids.len(),
filtered_count,
unknown_ids.len()
); );
Ok(SyncModelsResponse { Ok(SyncModelsResponse {
@ -898,19 +942,21 @@ impl AppService {
.filter(|m| m.id != "openrouter/auto") .filter(|m| m.id != "openrouter/auto")
.collect(); .collect();
// Identify "stranger" models: accessible from the user's endpoint but
// NOT present in OpenRouter's public catalog.
let or_matched_ids: std::collections::HashSet<&str> = filtered
.iter()
.map(|m| m.id.as_str())
.collect();
let unknown_ids: Vec<&str> = available_ids
.iter()
.filter(|id| !or_matched_ids.contains(id.as_str()))
.map(|s| s.as_str())
.collect();
let filtered_count = filtered.len(); let filtered_count = filtered.len();
// Fallback: if no OpenRouter metadata matches, sync models directly from // Sync stranger models (non-OpenRouter) through the direct pipeline.
// the user's endpoint (handles Bailian/MiniMax and other non-OpenRouter providers).
if filtered_count == 0 && !available_ids.is_empty() {
tracing::info!(
model_count = available_ids.len(),
"OpenRouter model sync: no matches, falling back to direct sync"
);
sync_models_direct(db, &available_ids).await;
return;
}
let mut models_created = 0i64; let mut models_created = 0i64;
let mut models_updated = 0i64; let mut models_updated = 0i64;
let mut versions_created = 0i64; let mut versions_created = 0i64;
@ -918,6 +964,39 @@ impl AppService {
let mut capabilities_created = 0i64; let mut capabilities_created = 0i64;
let mut profiles_created = 0i64; let mut profiles_created = 0i64;
if !unknown_ids.is_empty() {
tracing::info!(
unknown_count = unknown_ids.len(),
"OpenRouter model sync: {} models not in OpenRouter catalog, syncing directly",
unknown_ids.len()
);
let unknown_set: std::collections::HashSet<String> =
unknown_ids.iter().map(|s| ToString::to_string(s)).collect();
let direct_result = sync_models_direct(db, &unknown_set).await;
models_created += direct_result.models_created;
models_updated += direct_result.models_updated;
versions_created += direct_result.versions_created;
pricing_created += direct_result.pricing_created;
capabilities_created += direct_result.capabilities_created;
profiles_created += direct_result.profiles_created;
}
// If no OpenRouter metadata matched at all, the direct sync above
// already handled everything — return early.
if filtered_count == 0 {
tracing::info!(
matched = filtered_count,
models_created,
models_updated,
versions_created,
pricing_created,
capabilities_created,
profiles_created,
"OpenRouter model sync complete (direct only)"
);
return;
}
for or_model in filtered { for or_model in filtered {
let provider_slug = extract_provider(&or_model.id); let provider_slug = extract_provider(&or_model.id);
let provider = match upsert_provider(db, provider_slug).await { let provider = match upsert_provider(db, provider_slug).await {
@ -987,13 +1066,17 @@ impl AppService {
tracing::info!( tracing::info!(
matched = filtered_count, matched = filtered_count,
unknown = unknown_ids.len(),
models_created, models_created,
models_updated, models_updated,
versions_created, versions_created,
pricing_created, pricing_created,
capabilities_created, capabilities_created,
profiles_created, profiles_created,
"OpenRouter model sync complete" "OpenRouter model sync complete: {} total ({} OpenRouter + {} direct)",
filtered_count + unknown_ids.len(),
filtered_count,
unknown_ids.len()
); );
} }
} }