feat(models-dev): 高级设置新增 models.dev 在线同步 — 上下文/能力/单价即时生效 + 一键恢复内置;修复 public 快照构建漂移;确认适配 kimi-code 2.1.0 — bump to v0.7.21
Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s

This commit is contained in:
KimiSwitch Dev committed 2026-09-24 00:19:10 +08:00
1 parent 26e783c2ba
commit 11286419b1
17 files changed
+282395 -267694

No files matched your search

+59 -44
View File
@@ -6,7 +6,8 @@ use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::SystemTime;
use walkdir::WalkDir;
// ---------------------------------------------------------------------------
@@ -534,50 +535,62 @@ struct ModelsDevCost {
cache_write: Option<f64>,
}
/// Compiled-in models.dev snapshot (`src/lib/models-dev.json`, generated by
/// scripts/fetch-models-dev.mjs before every build via the `pretauri` hook).
/// Keyed by "<provider>/<model>", lowercased.
const MODELS_DEV_SNAPSHOT: &str = include_str!("../../src/lib/models-dev.json");
fn models_dev_cost_index() -> &'static HashMap<String, ModelsDevCost> {
static INDEX: OnceLock<HashMap<String, ModelsDevCost>> = OnceLock::new();
INDEX.get_or_init(|| {
let mut map = HashMap::new();
let Ok(v) = serde_json::from_str::<serde_json::Value>(MODELS_DEV_SNAPSHOT) else {
return map;
};
let Some(obj) = v.as_object() else {
return map;
};
for (key, entry) in obj {
// Skip the "last_updated" metadata key and entries without cost.
if !entry.is_object() || entry.get("cost").is_none() {
continue;
/// models.dev price index over the effective snapshot (runtime-synced copy
/// when present, else the compiled-in `src/lib/models-dev.json` generated by
/// scripts/fetch-models-dev.mjs). Keyed by "<provider>/<model>", lowercased.
/// Cached and rebuilt only when the synced file's mtime changes, so an online
/// sync (models_dev::sync_models_dev) refreshes prices without a restart.
fn models_dev_cost_index() -> Arc<HashMap<String, ModelsDevCost>> {
static CACHE: OnceLock<RwLock<Option<(Option<SystemTime>, Arc<HashMap<String, ModelsDevCost>>)>>>=
OnceLock::new();
let cache = CACHE.get_or_init(|| RwLock::new(None));
let stamp = fs::metadata(crate::models_dev::synced_path())
.and_then(|m| m.modified())
.ok();
{
let cached = cache.read().expect("price index lock");
if let Some((cached_stamp, idx)) = cached.as_ref() {
if *cached_stamp == stamp {
return idx.clone();
}
let Some(cost) = entry.get("cost").and_then(|c| c.as_object()) else {
continue;
};
let num = |k: &str| cost.get(k).and_then(|x| x.as_f64());
let (Some(input), Some(output)) = (num("input"), num("output")) else {
continue;
};
map.insert(
key.to_ascii_lowercase(),
ModelsDevCost {
input,
output,
cache_read: num("cache_read").unwrap_or(0.0),
cache_write: num("cache_write"),
},
);
}
map
})
}
let mut map = HashMap::new();
let v = serde_json::from_str::<serde_json::Value>(&crate::models_dev::effective_snapshot());
if let Ok(v) = v {
if let Some(obj) = v.as_object() {
for (key, entry) in obj {
// Skip the "last_updated" metadata key and entries without cost.
if !entry.is_object() || entry.get("cost").is_none() {
continue;
}
let Some(cost) = entry.get("cost").and_then(|c| c.as_object()) else {
continue;
};
let num = |k: &str| cost.get(k).and_then(|x| x.as_f64());
let (Some(input), Some(output)) = (num("input"), num("output")) else {
continue;
};
map.insert(
key.to_ascii_lowercase(),
ModelsDevCost {
input,
output,
cache_read: num("cache_read").unwrap_or(0.0),
cache_write: num("cache_write"),
},
);
}
}
}
let idx = Arc::new(map);
*cache.write().expect("price index lock") = Some((stamp, idx.clone()));
idx
}
/// Warm the compiled-in models.dev price index off the first dashboard open:
/// parsing the ~1.3 MB snapshot costs tens of milliseconds, so build it on a
/// background thread at startup instead of lazily on the first get_summary.
/// Warm the models.dev price index off the first dashboard open: parsing the
/// ~1.3 MB snapshot costs tens of milliseconds, so build it on a background
/// thread at startup instead of lazily on the first get_summary.
pub fn warm_price_index() {
std::thread::spawn(|| {
let _ = models_dev_cost_index();
@@ -608,14 +621,15 @@ fn provider_rank(key: &str) -> usize {
/// (deterministic tie-break). Returns None when nothing matches.
fn models_dev_lookup(model_name: &str) -> Option<(String, ModelsDevCost)> {
let lower = model_name.to_ascii_lowercase();
if let Some(cost) = models_dev_cost_index().get(&lower) {
let index = models_dev_cost_index();
if let Some(cost) = index.get(&lower) {
return Some((lower, *cost));
}
let bare = model_name.rsplit_once('/').map(|(_, b)| b).unwrap_or(model_name);
let bare_l = bare.to_ascii_lowercase();
// Match on the model part after the last '/', not the full key: aliases
// strip the family prefix (record "k2.5" vs models.dev "kimi-k2.5").
let mut matches: Vec<(&String, &ModelsDevCost)> = models_dev_cost_index()
let mut matches: Vec<(&String, &ModelsDevCost)> = index
.iter()
.filter(|(key, _)| {
key.rsplit_once('/')
@@ -695,7 +709,8 @@ fn match_price(model_name: &str) -> (String, f64, f64, f64, bool) {
fn priced_suffix_lookup(model_name: &str) -> Option<(String, ModelsDevCost)> {
let bare = model_name.rsplit_once('/').map(|(_, b)| b).unwrap_or(model_name);
let bare_l = bare.to_ascii_lowercase();
let mut matches: Vec<(&String, &ModelsDevCost)> = models_dev_cost_index()
let index = models_dev_cost_index();
let mut matches: Vec<(&String, &ModelsDevCost)> = index
.iter()
.filter(|(key, cost)| {
(cost.input > 0.0 || cost.output > 0.0)