Files
KimiCodeSwitch/src/lib/models-dev.ts
T
KimiSwitch Dev 1fecb161a9 perf(dashboard): 仪表盘加载提速
- models-dev 快照移出主 bundle(1.35MB→515KB),改为 /models-dev.json 运行时异步加载,就绪后重渲染
- Rust 启动时后台预热价格索引(warm_price_index),首次 get_summary 不再卡
- get_summary 扫描结果 8s TTL 缓存(按 home),切 tab/改范围复用;刷新按钮 refresh=true 强制重扫
2026-08-01 19:37:20 +08:00

117 lines
3.7 KiB
TypeScript

/**
* Compact per-model reference data from models.dev
* (see scripts/fetch-models-dev.mjs).
*
* The snapshot (~1.3 MB JSON) is NOT bundled into the main chunk anymore:
* parsing a 1.3 MB JSON literal at startup blocks first paint. It is served
* as a static asset (`/models-dev.json`) and loaded once in the background.
* `getModelRef` stays synchronous and returns undefined until the index is
* ready — callers already fall back to defaults — and `modelsDevReady()`
* lets the app re-render once the index arrives.
*/
export interface ModelCost {
input?: number;
output?: number;
cache_read?: number;
cache_write?: number;
}
export interface ModelRef {
name?: string;
context?: number;
reasoning?: boolean;
tool_call?: boolean;
structured_output?: boolean;
image?: boolean;
video?: boolean;
/** $/M tokens from models.dev (added by fetch-models-dev.mjs). */
cost?: ModelCost;
}
let byLowerKey: Record<string, ModelRef> | null = null;
let readyListeners: Array<() => void> = [];
function buildIndex(raw: Record<string, unknown>) {
const data: Record<string, ModelRef> = {};
for (const [key, value] of Object.entries(raw)) {
if (key !== "last_updated" && value && typeof value === "object") {
data[key] = value as ModelRef;
}
}
// models.dev keys are "<lab>/<model>" and may be mixed-case (e.g.
// "minimax/MiniMax-M3"), while provider APIs return bare model ids
// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching.
const lower: Record<string, ModelRef> = {};
for (const [key, value] of Object.entries(data)) {
lower[key.toLowerCase()] = value;
}
return lower;
}
let loadPromise: Promise<void> | null = null;
export function modelsDevReady(): Promise<void> {
if (!loadPromise) {
loadPromise = fetch(`${import.meta.env.BASE_URL}models-dev.json`)
.then((r) => {
if (!r.ok) throw new Error(`models-dev.json HTTP ${r.status}`);
return r.json() as Promise<Record<string, unknown>>;
})
.then((raw) => {
byLowerKey = buildIndex(raw);
const listeners = readyListeners;
readyListeners = [];
for (const cb of listeners) cb();
})
.catch((err) => {
// A failed load is permanent for this session: fall back to defaults.
byLowerKey = {};
loadPromise = null; // allow one retry next time
console.warn("models-dev.json load failed:", err);
});
}
return loadPromise;
}
/** Register a callback invoked once the models.dev index is ready. */
export function onModelsDevReady(cb: () => void): void {
if (byLowerKey) {
cb();
return;
}
readyListeners.push(cb);
}
/**
* Look up reference data for a provider model id.
* Matches exact key first, then any "<lab>/<id>" suffix.
* Returns undefined when nothing matches — callers fall back to
* getDefaultMaxContextSize / manual capabilities.
*/
export function getModelRef(modelId: string): ModelRef | undefined {
const id = modelId.trim().toLowerCase();
if (!id || !byLowerKey) return undefined;
const exact = byLowerKey[id];
if (exact) return exact;
const suffix = `/${id}`;
for (const [key, value] of Object.entries(byLowerKey)) {
if (key.endsWith(suffix)) return value;
}
return undefined;
}
/**
* Map reference data to Kimi Code capability flags.
* Multimodal (image_in/video_in) comes from models.dev input modalities;
* always_thinking cannot be derived and stays manual.
*/
export function capabilitiesFromRef(ref: ModelRef): string[] {
const caps: string[] = [];
if (ref.reasoning) caps.push("thinking");
if (ref.tool_call) caps.push("tool_use");
if (ref.image) caps.push("image_in");
if (ref.video) caps.push("video_in");
return caps;
}