From 638dfd7a6eb9179bb816efdc2a951719b3909051 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Sun, 26 Apr 2026 15:17:33 +0800 Subject: [PATCH] feat(agent/sync): sync non-OpenRouter models from upstream endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- libs/service/agent/sync.rs | 135 ++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 26 deletions(-) diff --git a/libs/service/agent/sync.rs b/libs/service/agent/sync.rs index d96434a..ed29257 100644 --- a/libs/service/agent/sync.rs +++ b/libs/service/agent/sync.rs @@ -5,7 +5,10 @@ //! 2. Fetch full metadata (pricing, context_length, capabilities) for those //! model IDs from OpenRouter's public `/api/v1/models` endpoint (no auth). //! 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 //! immediately and then every 10 minutes. On app startup, run it once @@ -704,7 +707,11 @@ impl AppService { /// Steps: /// 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. - /// 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( &self, _ctx: &Session, @@ -733,17 +740,21 @@ impl AppService { .filter(|m| m.id != "openrouter/auto") .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 - // 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 filtered_count = filtered.len(); let mut models_created = 0i64; let mut models_updated = 0i64; @@ -752,6 +763,37 @@ impl AppService { let mut capabilities_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 = + 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 { let provider_slug = extract_provider(&or_model.id); let provider = match upsert_provider(&self.db, provider_slug).await { @@ -823,8 +865,10 @@ impl AppService { filtered_count, models_created, models_updated, - "sync_upstream_models: synced {} accessible models", - filtered_count + "sync_upstream_models: synced {} accessible models ({}) OpenRouter + ({}) direct", + filtered_count + unknown_ids.len(), + filtered_count, + unknown_ids.len() ); Ok(SyncModelsResponse { @@ -898,19 +942,21 @@ impl AppService { .filter(|m| m.id != "openrouter/auto") .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(); - // Fallback: if no OpenRouter metadata matches, sync models directly from - // 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; - } - + // Sync stranger models (non-OpenRouter) through the direct pipeline. let mut models_created = 0i64; let mut models_updated = 0i64; let mut versions_created = 0i64; @@ -918,6 +964,39 @@ impl AppService { let mut capabilities_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 = + 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 { let provider_slug = extract_provider(&or_model.id); let provider = match upsert_provider(db, provider_slug).await { @@ -987,13 +1066,17 @@ impl AppService { tracing::info!( matched = filtered_count, + unknown = unknown_ids.len(), models_created, models_updated, versions_created, pricing_created, capabilities_created, profiles_created, - "OpenRouter model sync complete" + "OpenRouter model sync complete: {} total ({} OpenRouter + {} direct)", + filtered_count + unknown_ids.len(), + filtered_count, + unknown_ids.len() ); } }