refactor(service): remove all hardcoded model-name inference from OpenRouter sync

Drop all hard-coded model-name lookup tables that hardcoded specific
model names and prices:
- infer_context_length: remove GPT-4o/o1/Claude/etc. fallback table
- infer_max_output: remove GPT-4o/o1/etc. output token limits
- infer_pricing_fallback: remove entire hardcoded pricing table
- infer_capability_list: derive from architecture.modality only,
  no longer uses model name strings

Also fix stats: if upsert_version fails, skip counting and continue
rather than counting model but not version (which caused
versions_created=0 while pricing_created>0 inconsistency).
This commit is contained in:
ZhenYi 2026-04-16 22:47:24 +08:00
parent 0a998affbb
commit 3a30150a41

View File

@ -157,131 +157,32 @@ fn infer_capability(name: &str) -> ModelCapability {
}
}
fn infer_context_length(name: &str) -> i64 {
let lower = name.to_lowercase();
// Hard-coded fallback table for known models
let fallbacks: &[(&str, i64)] = &[
("gpt-4o", 128_000),
("chatgpt-4o", 128_000),
("o1-preview", 128_000),
("o1-mini", 65_536),
("o1", 65_536),
("o3-mini", 65_536),
("gpt-4-turbo", 128_000),
("gpt-4-32k", 32_768),
("gpt-4", 8_192),
("gpt-4o-mini", 128_000),
("chatgpt-4o-mini", 128_000),
("gpt-3.5-turbo-16k", 16_384),
("gpt-3.5-turbo", 16_385),
("text-embedding-3-large", 8_191),
("text-embedding-3-small", 8_191),
("text-embedding-ada", 8_191),
("dall-e", 4_096),
("whisper", 30_000),
("gpt-image-1", 16_384),
];
for (prefix, ctx) in fallbacks {
if lower.starts_with(prefix) {
return *ctx;
}
}
8_192
fn infer_context_length(ctx: Option<u64>) -> i64 {
ctx.map(|c| c as i64).unwrap_or(8_192)
}
fn infer_max_output(name: &str, top_provider_max: Option<u64>) -> Option<i64> {
if let Some(v) = top_provider_max {
return Some(v as i64);
}
let lower = name.to_lowercase();
let fallbacks: &[(&str, i64)] = &[
("gpt-4o", 16_384),
("chatgpt-4o", 16_384),
("o1-preview", 32_768),
("o1-mini", 65_536),
("o1", 100_000),
("o3-mini", 100_000),
("gpt-4-turbo", 4_096),
("gpt-4-32k", 32_768),
("gpt-4", 8_192),
("gpt-4o-mini", 16_384),
("chatgpt-4o-mini", 16_384),
("gpt-3.5-turbo", 4_096),
("gpt-image-1", 1_024),
];
for (prefix, max) in fallbacks {
if lower.starts_with(prefix) {
return Some(*max);
}
}
if lower.starts_with("gpt") || lower.starts_with("o1") || lower.starts_with("o3") {
Some(4_096)
} else {
None
}
fn infer_max_output(top_provider_max: Option<u64>) -> Option<i64> {
top_provider_max.map(|v| v as i64)
}
fn infer_capability_list(name: &str) -> Vec<(CapabilityType, bool)> {
let lower = name.to_lowercase();
let mut caps = Vec::new();
caps.push((CapabilityType::FunctionCall, true));
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)];
if lower.contains("gpt-") || lower.contains("o1") || lower.contains("o3") {
caps.push((CapabilityType::ToolUse, true));
}
if lower.contains("vision")
|| lower.contains("gpt-4o")
|| lower.contains("gpt-image")
|| lower.contains("dall-e")
{
// 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 lower.contains("o1") || lower.contains("o3") {
caps.push((CapabilityType::Reasoning, true));
if m.contains("text") || m.contains("chat") {
caps.push((CapabilityType::ToolUse, true));
}
}
caps
}
fn infer_pricing_fallback(name: &str) -> Option<(String, String)> {
let lower = name.to_lowercase();
if lower.contains("gpt-4o-mini") || lower.contains("chatgpt-4o-mini") {
Some(("0.075".to_string(), "0.30".to_string()))
} else if lower.contains("gpt-4o") || lower.contains("chatgpt-4o") {
Some(("2.50".to_string(), "10.00".to_string()))
} else if lower.contains("gpt-4-turbo") {
Some(("10.00".to_string(), "30.00".to_string()))
} else if lower.contains("gpt-4") && !lower.contains("4o") {
Some(("15.00".to_string(), "60.00".to_string()))
} else if lower.contains("gpt-3.5-turbo") {
Some(("0.50".to_string(), "1.50".to_string()))
} else if lower.contains("o1-preview") {
Some(("15.00".to_string(), "60.00".to_string()))
} else if lower.contains("o1-mini") {
Some(("3.00".to_string(), "12.00".to_string()))
} else if lower.contains("o1") {
Some(("15.00".to_string(), "60.00".to_string()))
} else if lower.contains("o3-mini") {
Some(("1.50".to_string(), "6.00".to_string()))
} else if lower.contains("embedding-3-small") {
Some(("0.02".to_string(), "0.00".to_string()))
} else if lower.contains("embedding-3-large") {
Some(("0.13".to_string(), "0.00".to_string()))
} else if lower.contains("embedding-ada") {
Some(("0.10".to_string(), "0.00".to_string()))
} else if lower.contains("embedding") {
Some(("0.10".to_string(), "0.00".to_string()))
} else if lower.contains("dall-e") {
Some(("0.00".to_string(), "4.00".to_string()))
} else if lower.contains("whisper") {
Some(("0.00".to_string(), "0.006".to_string()))
} else {
None
}
}
// Provider helpers -----------------------------------------------------------
/// Extract provider slug from OpenRouter model ID (e.g. "anthropic/claude-3.5-sonnet" → "anthropic").
@ -372,13 +273,10 @@ async fn upsert_model(
let capability = infer_capability(model_id_str);
// OpenRouter context_length takes priority; fall back to inference
let context_length = or_model
.context_length
.map(|c| c as i64)
.unwrap_or_else(|| infer_context_length(model_id_str));
let context_length = infer_context_length(or_model.context_length);
let max_output =
infer_max_output(model_id_str, or_model.top_provider.as_ref().and_then(|p| p.max_completion_tokens));
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()
@ -450,7 +348,6 @@ async fn upsert_pricing(
db: &AppDatabase,
version_uuid: Uuid,
pricing: Option<&OpenRouterPricing>,
model_name: &str,
) -> Result<bool, AppError> {
use models::agents::model_pricing::Column as PCol;
let existing = PricingEntity::find()
@ -461,11 +358,9 @@ async fn upsert_pricing(
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 {
// OpenRouter prices are per-million-tokens strings
(p.prompt.clone(), p.completion.clone())
} else if let Some((i, o)) = infer_pricing_fallback(model_name) {
(i, o)
} else {
("0.00".to_string(), "0.00".to_string())
};
@ -486,10 +381,16 @@ async fn upsert_pricing(
async fn upsert_capabilities(
db: &AppDatabase,
version_uuid: Uuid,
model_name: &str,
arch: Option<&OpenRouterArchitecture>,
) -> Result<i64, AppError> {
use models::agents::model_capability::Column as CCol;
let caps = infer_capability_list(model_name);
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;
@ -559,8 +460,7 @@ impl AppService {
/// parameter-profile records.
///
/// OpenRouter returns `context_length`, `pricing`, and `architecture.modality`
/// per model — these drive all inference-free field population.
/// Capabilities are still inferred from model name patterns.
/// per model — these drive all field population. No model names are hardcoded.
pub async fn sync_upstream_models(
&self,
_ctx: &Session,
@ -603,26 +503,29 @@ impl AppService {
}
let (version_record, version_is_new) =
upsert_version(&self.db, model_record.id).await?;
match upsert_version(&self.db, model_record.id).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("sync_upstream_models: upsert_version error: {:?}", e);
continue;
}
};
if version_is_new {
versions_created += 1;
}
if upsert_pricing(
&self.db,
version_record.id,
or_model.pricing.as_ref(),
&or_model.id,
)
.await?
{
if let Err(e) = upsert_pricing(&self.db, version_record.id, or_model.pricing.as_ref()).await {
tracing::warn!("sync_upstream_models: upsert_pricing error: {:?}", e);
} else {
pricing_created += 1;
}
capabilities_created +=
upsert_capabilities(&self.db, version_record.id, &or_model.id).await?;
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? {
if upsert_parameter_profile(&self.db, version_record.id, &or_model.id).await.unwrap_or(false) {
profiles_created += 1;
}
}
@ -720,13 +623,18 @@ impl AppService {
}
};
match upsert_version(db, model_record.id).await {
Ok((version_record, version_is_new)) => {
let (version_record, version_is_new) = match upsert_version(db, model_record.id).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("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(), &or_model.id)
if upsert_pricing(db, version_record.id, or_model.pricing.as_ref())
.await
.unwrap_or(false)
{
@ -734,7 +642,7 @@ impl AppService {
}
capabilities_created +=
upsert_capabilities(db, version_record.id, &or_model.id)
upsert_capabilities(db, version_record.id, or_model.architecture.as_ref())
.await
.unwrap_or(0);
@ -745,11 +653,6 @@ impl AppService {
profiles_created += 1;
}
}
Err(e) => {
tracing::warn!("OpenRouter model sync: upsert_version error: {:?}", e);
}
}
}
tracing::info!(
"OpenRouter model sync complete: created={} updated={} \