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:
parent
0a998affbb
commit
3a30150a41
@ -157,131 +157,32 @@ fn infer_capability(name: &str) -> ModelCapability {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn infer_context_length(name: &str) -> i64 {
|
fn infer_context_length(ctx: Option<u64>) -> i64 {
|
||||||
let lower = name.to_lowercase();
|
ctx.map(|c| c as i64).unwrap_or(8_192)
|
||||||
// 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_max_output(name: &str, top_provider_max: Option<u64>) -> Option<i64> {
|
fn infer_max_output(top_provider_max: Option<u64>) -> Option<i64> {
|
||||||
if let Some(v) = top_provider_max {
|
top_provider_max.map(|v| v as i64)
|
||||||
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_capability_list(name: &str) -> Vec<(CapabilityType, bool)> {
|
fn infer_capability_list(arch: &OpenRouterArchitecture) -> Vec<(CapabilityType, bool)> {
|
||||||
let lower = name.to_lowercase();
|
// Derive capabilities purely from OpenRouter architecture data.
|
||||||
let mut caps = Vec::new();
|
// FunctionCall is a safe baseline for chat models.
|
||||||
caps.push((CapabilityType::FunctionCall, true));
|
let mut caps = vec![(CapabilityType::FunctionCall, true)];
|
||||||
|
|
||||||
if lower.contains("gpt-") || lower.contains("o1") || lower.contains("o3") {
|
// Vision capability from modality.
|
||||||
caps.push((CapabilityType::ToolUse, true));
|
if let Some(m) = &arch.modality {
|
||||||
}
|
let m = m.to_lowercase();
|
||||||
|
if m.contains("image") || m.contains("vision") {
|
||||||
if lower.contains("vision")
|
|
||||||
|| lower.contains("gpt-4o")
|
|
||||||
|| lower.contains("gpt-image")
|
|
||||||
|| lower.contains("dall-e")
|
|
||||||
{
|
|
||||||
caps.push((CapabilityType::Vision, true));
|
caps.push((CapabilityType::Vision, true));
|
||||||
}
|
}
|
||||||
|
if m.contains("text") || m.contains("chat") {
|
||||||
if lower.contains("o1") || lower.contains("o3") {
|
caps.push((CapabilityType::ToolUse, true));
|
||||||
caps.push((CapabilityType::Reasoning, true));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
caps
|
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 -----------------------------------------------------------
|
// Provider helpers -----------------------------------------------------------
|
||||||
|
|
||||||
/// Extract provider slug from OpenRouter model ID (e.g. "anthropic/claude-3.5-sonnet" → "anthropic").
|
/// 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);
|
let capability = infer_capability(model_id_str);
|
||||||
|
|
||||||
// OpenRouter context_length takes priority; fall back to inference
|
// OpenRouter context_length takes priority; fall back to inference
|
||||||
let context_length = or_model
|
let context_length = infer_context_length(or_model.context_length);
|
||||||
.context_length
|
|
||||||
.map(|c| c as i64)
|
|
||||||
.unwrap_or_else(|| infer_context_length(model_id_str));
|
|
||||||
|
|
||||||
let max_output =
|
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;
|
use models::agents::model::Column as MCol;
|
||||||
if let Some(existing) = ModelEntity::find()
|
if let Some(existing) = ModelEntity::find()
|
||||||
@ -450,7 +348,6 @@ async fn upsert_pricing(
|
|||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
version_uuid: Uuid,
|
version_uuid: Uuid,
|
||||||
pricing: Option<&OpenRouterPricing>,
|
pricing: Option<&OpenRouterPricing>,
|
||||||
model_name: &str,
|
|
||||||
) -> Result<bool, AppError> {
|
) -> Result<bool, AppError> {
|
||||||
use models::agents::model_pricing::Column as PCol;
|
use models::agents::model_pricing::Column as PCol;
|
||||||
let existing = PricingEntity::find()
|
let existing = PricingEntity::find()
|
||||||
@ -461,11 +358,9 @@ async fn upsert_pricing(
|
|||||||
return Ok(false);
|
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 {
|
let (input_str, output_str) = if let Some(p) = pricing {
|
||||||
// OpenRouter prices are per-million-tokens strings
|
|
||||||
(p.prompt.clone(), p.completion.clone())
|
(p.prompt.clone(), p.completion.clone())
|
||||||
} else if let Some((i, o)) = infer_pricing_fallback(model_name) {
|
|
||||||
(i, o)
|
|
||||||
} else {
|
} else {
|
||||||
("0.00".to_string(), "0.00".to_string())
|
("0.00".to_string(), "0.00".to_string())
|
||||||
};
|
};
|
||||||
@ -486,10 +381,16 @@ async fn upsert_pricing(
|
|||||||
async fn upsert_capabilities(
|
async fn upsert_capabilities(
|
||||||
db: &AppDatabase,
|
db: &AppDatabase,
|
||||||
version_uuid: Uuid,
|
version_uuid: Uuid,
|
||||||
model_name: &str,
|
arch: Option<&OpenRouterArchitecture>,
|
||||||
) -> Result<i64, AppError> {
|
) -> Result<i64, AppError> {
|
||||||
use models::agents::model_capability::Column as CCol;
|
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 now = Utc::now();
|
||||||
let mut created = 0i64;
|
let mut created = 0i64;
|
||||||
|
|
||||||
@ -559,8 +460,7 @@ impl AppService {
|
|||||||
/// 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 field population. No model names are hardcoded.
|
||||||
/// Capabilities are still inferred from model name patterns.
|
|
||||||
pub async fn sync_upstream_models(
|
pub async fn sync_upstream_models(
|
||||||
&self,
|
&self,
|
||||||
_ctx: &Session,
|
_ctx: &Session,
|
||||||
@ -603,26 +503,29 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (version_record, version_is_new) =
|
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 {
|
if version_is_new {
|
||||||
versions_created += 1;
|
versions_created += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if upsert_pricing(
|
if let Err(e) = upsert_pricing(&self.db, version_record.id, or_model.pricing.as_ref()).await {
|
||||||
&self.db,
|
tracing::warn!("sync_upstream_models: upsert_pricing error: {:?}", e);
|
||||||
version_record.id,
|
} else {
|
||||||
or_model.pricing.as_ref(),
|
|
||||||
&or_model.id,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
pricing_created += 1;
|
pricing_created += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
capabilities_created +=
|
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;
|
profiles_created += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -720,13 +623,18 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match upsert_version(db, model_record.id).await {
|
let (version_record, version_is_new) = match upsert_version(db, model_record.id).await {
|
||||||
Ok((version_record, version_is_new)) => {
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("OpenRouter model sync: upsert_version error: {:?}", e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
if version_is_new {
|
if version_is_new {
|
||||||
versions_created += 1;
|
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
|
.await
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
@ -734,7 +642,7 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
capabilities_created +=
|
capabilities_created +=
|
||||||
upsert_capabilities(db, version_record.id, &or_model.id)
|
upsert_capabilities(db, version_record.id, or_model.architecture.as_ref())
|
||||||
.await
|
.await
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@ -745,11 +653,6 @@ impl AppService {
|
|||||||
profiles_created += 1;
|
profiles_created += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("OpenRouter model sync: upsert_version error: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"OpenRouter model sync complete: created={} updated={} \
|
"OpenRouter model sync complete: created={} updated={} \
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user