merge
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

This commit is contained in:
2026-03-06 15:22:06 -05:00
parent a243a3987d
commit e8955fd36c
13 changed files with 150 additions and 25 deletions
+10
View File
@@ -109,6 +109,8 @@ pub async fn run_migrations(pool: &DbPool) -> Result<()> {
enabled BOOLEAN DEFAULT TRUE,
prompt_cost_per_m REAL,
completion_cost_per_m REAL,
cache_read_cost_per_m REAL,
cache_write_cost_per_m REAL,
mapping TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (provider_id) REFERENCES provider_configs(id) ON DELETE CASCADE
@@ -180,6 +182,14 @@ pub async fn run_migrations(pool: &DbPool) -> Result<()> {
.execute(pool)
.await;
// Add manual cache cost columns to model_configs if they don't exist
let _ = sqlx::query("ALTER TABLE model_configs ADD COLUMN cache_read_cost_per_m REAL")
.execute(pool)
.await;
let _ = sqlx::query("ALTER TABLE model_configs ADD COLUMN cache_write_cost_per_m REAL")
.execute(pool)
.await;
// Insert default admin user if none exists (default password: admin)
let user_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users").fetch_one(pool).await?;
+4 -6
View File
@@ -152,8 +152,11 @@ impl super::Provider for DeepSeekProvider {
// Sanitize and fix for deepseek-reasoner (R1)
if request.model == "deepseek-reasoner" {
if let Some(obj) = body.as_object_mut() {
obj.remove("stream_options");
// Keep stream_options if present (DeepSeek supports include_usage)
// Remove unsupported parameters
obj.remove("temperature");
obj.remove("top_p");
obj.remove("presence_penalty");
obj.remove("frequency_penalty");
@@ -177,11 +180,6 @@ impl super::Provider for DeepSeekProvider {
}
}
}
} else {
// For standard deepseek-chat, keep it clean
if let Some(obj) = body.as_object_mut() {
obj.remove("stream_options");
}
}
let url = format!("{}/chat/completions", self.config.base_url);
+1 -2
View File
@@ -317,8 +317,7 @@ impl super::Provider for OpenAIProvider {
// Standard OpenAI cleanup
if let Some(obj) = body.as_object_mut() {
obj.remove("stream_options");
// stream_options.include_usage is supported by OpenAI for token usage in streaming
// Transition: Newer OpenAI models (o1, o3, gpt-5) require max_completion_tokens
if request.model.starts_with("o1-") || request.model.starts_with("o3-") || request.model.contains("gpt-5") {
if let Some(max_tokens) = obj.remove("max_tokens") {
+17 -2
View File
@@ -137,8 +137,23 @@ async fn get_model_cost(
// Check in-memory cache for cost overrides (no SQLite hit)
if let Some(cached) = state.model_config_cache.get(model).await {
if let (Some(p), Some(c)) = (cached.prompt_cost_per_m, cached.completion_cost_per_m) {
// Manual overrides don't have cache-specific rates, so use simple formula
return (prompt_tokens as f64 * p / 1_000_000.0) + (completion_tokens as f64 * c / 1_000_000.0);
// Manual overrides logic: if cache rates are provided, use cache-aware formula.
// Formula: (non_cached_prompt * input_rate) + (cache_read * read_rate) + (cache_write * write_rate) + (completion * output_rate)
let non_cached_prompt = prompt_tokens.saturating_sub(cache_read_tokens);
let mut total = (non_cached_prompt as f64 * p / 1_000_000.0) + (completion_tokens as f64 * c / 1_000_000.0);
if let Some(cr) = cached.cache_read_cost_per_m {
total += cache_read_tokens as f64 * cr / 1_000_000.0;
} else {
// No manual cache_read rate — charge cached tokens at full input rate (backwards compatibility)
total += cache_read_tokens as f64 * p / 1_000_000.0;
}
if let Some(cw) = cached.cache_write_cost_per_m {
total += cache_write_tokens as f64 * cw / 1_000_000.0;
}
return total;
}
}
+7 -3
View File
@@ -15,6 +15,8 @@ pub struct CachedModelConfig {
pub mapping: Option<String>,
pub prompt_cost_per_m: Option<f64>,
pub completion_cost_per_m: Option<f64>,
pub cache_read_cost_per_m: Option<f64>,
pub cache_write_cost_per_m: Option<f64>,
}
/// In-memory cache for model_configs table.
@@ -35,15 +37,15 @@ impl ModelConfigCache {
/// Load all model configs from the database into cache
pub async fn refresh(&self) {
match sqlx::query_as::<_, (String, bool, Option<String>, Option<f64>, Option<f64>)>(
"SELECT id, enabled, mapping, prompt_cost_per_m, completion_cost_per_m FROM model_configs",
match sqlx::query_as::<_, (String, bool, Option<String>, Option<f64>, Option<f64>, Option<f64>, Option<f64>)>(
"SELECT id, enabled, mapping, prompt_cost_per_m, completion_cost_per_m, cache_read_cost_per_m, cache_write_cost_per_m FROM model_configs",
)
.fetch_all(&self.db_pool)
.await
{
Ok(rows) => {
let mut map = HashMap::with_capacity(rows.len());
for (id, enabled, mapping, prompt_cost, completion_cost) in rows {
for (id, enabled, mapping, prompt_cost, completion_cost, cache_read_cost, cache_write_cost) in rows {
map.insert(
id,
CachedModelConfig {
@@ -51,6 +53,8 @@ impl ModelConfigCache {
mapping,
prompt_cost_per_m: prompt_cost,
completion_cost_per_m: completion_cost,
cache_read_cost_per_m: cache_read_cost,
cache_write_cost_per_m: cache_write_cost,
},
);
}
+16 -2
View File
@@ -118,8 +118,22 @@ where
// Check in-memory cache for cost overrides (no SQLite hit)
let cost = if let Some(cached) = config_cache.get(&model).await {
if let (Some(p), Some(c)) = (cached.prompt_cost_per_m, cached.completion_cost_per_m) {
// Cost override doesn't have cache-aware pricing, use simple formula
(prompt_tokens as f64 * p / 1_000_000.0) + (completion_tokens as f64 * c / 1_000_000.0)
// Manual overrides logic: if cache rates are provided, use cache-aware formula.
let non_cached_prompt = prompt_tokens.saturating_sub(cache_read_tokens);
let mut total = (non_cached_prompt as f64 * p / 1_000_000.0) + (completion_tokens as f64 * c / 1_000_000.0);
if let Some(cr) = cached.cache_read_cost_per_m {
total += cache_read_tokens as f64 * cr / 1_000_000.0;
} else {
// Charge cached tokens at full input rate if no specific rate provided
total += cache_read_tokens as f64 * p / 1_000_000.0;
}
if let Some(cw) = cached.cache_write_cost_per_m {
total += cache_write_tokens as f64 * cw / 1_000_000.0;
}
total
} else {
provider.calculate_cost(
&model,