LoomLensAI / loomlens-engine.js
Aster Vale
update: Per-Job pricing to $0.033/estimate — 1,000 jobs = $33 vs $15 Pro subscription
2dee62e
Raw History Blame Contribute Delete
13.3 kB
/**
* LoomLens Live — Cost Estimation Engine
* Pure JavaScript: no TypeScript, no build step required.
* Works as ES module, CommonJS, or browser global.
*
* Token estimation → cost calculation → cluster-aware fit scoring.
* Zero API calls. Zero dependencies.
*/
// ─── Inline Cluster Data ────────────────────────────────────────────────────
// (Cluster definitions are inlined at bottom of this file or injected by build)
const CLUSTERS = [
{
id: 'flash', name: 'Flash Reasoning',
models: [
{ id:'minimax/MiniMax-M2', name:'MiniMax M2', provider:'MiniMax', tier:3, latency:'fast', inputPricePerM:0.10, outputPricePerM:0.40 },
{ id:'google/gemini-2.5-flash', name:'Gemini 2.5 Flash', provider:'Google', tier:3, latency:'fast', inputPricePerM:0.15, outputPricePerM:0.60 },
{ id:'anthropic/claude-haiku', name:'Claude Haiku', provider:'Anthropic', tier:3, latency:'fast', inputPricePerM:0.80, outputPricePerM:4.00 },
{ id:'deepseek/deepseek-chat-v3',name:'DeepSeek Chat v3',provider:'DeepSeek', tier:3, latency:'fast', inputPricePerM:0.27, outputPricePerM:1.10 },
],
},
{
id: 'fastgen', name: 'Fast General',
models: [
{ id:'openai/gpt-4o-mini', name:'GPT-4o Mini', provider:'OpenAI', tier:3, latency:'fast', inputPricePerM:0.15, outputPricePerM:0.60 },
{ id:'google/gemini-2.0-flash', name:'Gemini 2.0 Flash', provider:'Google', tier:3, latency:'fast', inputPricePerM:0.10, outputPricePerM:0.40 },
{ id:'xai/grok-3', name:'xAI Grok-3', provider:'xAI', tier:3, latency:'fast', inputPricePerM:2.00, outputPricePerM:10.0 },
],
},
{
id: 'code', name: 'Balanced Coding',
models: [
{ id:'anthropic/claude-sonnet-4-20250514', name:'Claude Sonnet 4', provider:'Anthropic', tier:2, latency:'medium', inputPricePerM:3.00, outputPricePerM:15.0 },
{ id:'openai/gpt-4o', name:'GPT-4o', provider:'OpenAI', tier:2, latency:'medium', inputPricePerM:2.50, outputPricePerM:10.0 },
{ id:'anthropic/claude-3.5-sonnet', name:'Claude 3.5 Sonnet', provider:'Anthropic',tier:2, latency:'medium', inputPricePerM:3.00, outputPricePerM:15.0 },
],
},
{
id: 'power', name: 'Power / Architecture',
models: [
{ id:'anthropic/claude-opus-4-20250514', name:'Claude Opus 4', provider:'Anthropic', tier:1, latency:'slow', inputPricePerM:15.0, outputPricePerM:75.0 },
{ id:'anthropic/claude-code', name:'Claude Code', provider:'Anthropic', tier:1, latency:'slow', inputPricePerM:15.0, outputPricePerM:75.0 },
],
},
{
id: 'vision', name: 'Vision / Multimodal',
models: [
{ id:'openai/gpt-4o', name:'GPT-4o', provider:'OpenAI', tier:4, latency:'medium', inputPricePerM:2.50, outputPricePerM:10.0 },
{ id:'anthropic/claude-3.5', name:'Claude 3.5', provider:'Anthropic', tier:4, latency:'medium', inputPricePerM:3.00, outputPricePerM:15.0 },
],
},
];
const LATENCY_SCORE = { fast: 100, medium: 62, slow: 22 };
const CAPABILITY_FIT = {
simple_qa: { flash: 95, fastgen: 80, code: 30, power: 20, vision: 20 },
summary: { flash: 90, fastgen: 75, code: 40, power: 30, vision: 20 },
code_edit: { flash: 60, fastgen: 40, code: 90, power: 40, vision: 20 },
code_write: { flash: 30, fastgen: 20, code: 95, power: 95, vision: 20 },
complex_analysis: { flash: 40, fastgen: 30, code: 85, power: 98, vision: 20 },
vision: { flash: 10, fastgen: 10, code: 30, power: 30, vision: 98 },
unknown: { flash: 70, fastgen: 60, code: 60, power: 50, vision: 40 },
};
const TASK_KEYWORDS = {
simple_qa: ['what is','who is','define','explain','how do i','how does','what are','why does','when did','where is','tell me','give me'],
summary: ['summarize','tl;dr','sum up','brief','recap','key points','highlights','overview','condense','shorten'],
code_edit: ['fix','bug','debug','hotfix','patch','tweak','adjust','correct','error','issue'],
code_write: ['write','build','create','implement','add feature','new function','generate',' scaffold','code for','make a'],
complex_analysis: ['analyze','architecture','review','refactor','design','strategy','compare','evaluate','assess','plan'],
vision: ['image','photo','picture','screenshot','diagram','chart','visual','frame','pdf','document'],
};
const OUTPUT_TOKENS = { simple_qa:150, summary:200, code_edit:400, code_write:800, complex_analysis:600, vision:300, unknown:400 };
const CHARS_PER_TOKEN = { en: 3.8, es: 3.5, ja: 1.2, zh: 1.0 };
// ─── Token Estimation ────────────────────────────────────────────────────────
export function detectLanguage(text) {
if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) return 'ja';
if (/[\u4E00-\u9FFF]/.test(text)) return 'zh';
if (/[áéíóúñ¿¡ü]/i.test(text)) return 'es';
return 'en';
}
export function estimateInputTokens(text) {
if (!text || !text.trim()) return 0;
const lang = detectLanguage(text);
const cpt = CHARS_PER_TOKEN[lang];
const multiline = (text.match(/\n/g)||[]).length;
const codeFences = (text.match(/```/g)||[]).length;
const mult = (codeFences >= 2 || multiline > 3) ? 1.2 : 1.0;
return Math.ceil((text.length / cpt) * mult);
}
// ─── Task Detection ─────────────────────────────────────────────────────────
export function detectTaskType(prompt) {
const lower = prompt.toLowerCase();
const scores = {};
for (const [type, keywords] of Object.entries(TASK_KEYWORDS)) {
scores[type] = keywords.reduce((acc, kw) => acc + (lower.includes(kw) ? 1 : 0), 0);
}
if (/```[\s\S]+```/.test(prompt)) scores['code_write'] = (scores['code_write']||0) + 2;
if (/^\s*(import|from|const|let|function|class|def |fn |pub fn)/m.test(prompt)) scores['code_write'] = (scores['code_write']||0) + 2;
if (/\b(fix|bug|error|issue)\b/i.test(prompt)) scores['code_edit'] = (scores['code_edit']||0) + 1;
if (/\b(write|build|create|implement)\b/i.test(prompt)) scores['code_write'] = (scores['code_write']||0) + 1;
if (/\b(analyze|evaluate|compare|assess)\b/i.test(prompt)) scores['complex_analysis'] = (scores['complex_analysis']||0) + 1;
const best = Object.entries(scores).sort((a,b) => (b[1]??0) - (a[1]??0))[0];
const score = best?.[1] ?? 0;
const type = score === 0 ? 'unknown' : best[0];
return { type, confidence: Math.min(score/3, 1), keywords: (TASK_KEYWORDS[type]??[]).filter(kw=>lower.includes(kw)) };
}
// ─── Cost ──────────────────────────────────────────────────────────────────
export function calcCost(inPrice, outPrice, inTokens, outTokens) {
return { inputCost: Math.round((inTokens/1e6)*inPrice*1e6)/1e6, outputCost: Math.round((outTokens/1e6)*outPrice*1e6)/1e6,
totalCost: Math.round(((inTokens/1e6)*inPrice + (outTokens/1e6)*outPrice)*1e6)/1e6 };
}
// ─── Fit Scoring ────────────────────────────────────────────────────────────
export function scoreWithinCluster(model, task, clusterModels) {
const capFit = CAPABILITY_FIT[task]?.[model.clusterId] ?? 50;
const costs = clusterModels.map(m => calcCost(m.inputPricePerM, m.outputPricePerM, 1, 1).totalCost);
const minC = Math.min(...costs);
const maxC = Math.max(...costs);
const range = maxC - minC || 1;
const costEff = Math.round(100 - ((calcCost(model.inputPricePerM, model.outputPricePerM, 1, 1).totalCost - minC) / range * 100));
const speed = LATENCY_SCORE[model.latency] ?? 50;
return Math.round(capFit * 0.35 + costEff * 0.35 + speed * 0.30);
}
export function getInsight(modelName, clusterName, clusterId, task, score, isBest) {
const labels = { simple_qa:'quick Q&A', summary:'summarization', code_edit:'code fixes', code_write:'code builds',
complex_analysis:'deep analysis', vision:'image/doc understanding', unknown:'general purpose' };
const label = labels[task] ?? 'general purpose';
if (isBest) return `Best in ${clusterName} for ${label}`;
if (score >= 80) return `Strong fit for ${label}`;
if (score >= 65) return `Capable for ${label}`;
return `Consider alternatives for ${label}`;
}
// ─── Main Estimator ─────────────────────────────────────────────────────────
/**
* estimatePrompt(prompt, selectedModelId?) → LoomLensResult
*/
export function estimatePrompt(prompt, selectedModelId = '') {
const task = detectTaskType(prompt);
const inTokens = estimateInputTokens(prompt);
const outTokens = OUTPUT_TOKENS[task.type] ?? 400;
const allFlat = CLUSTERS.flatMap(c => c.models.map(m => ({ ...m, clusterId: c.id, clusterName: c.name })));
const clusters = {};
const allModels = [];
for (const cluster of CLUSTERS) {
const clusterModels = allFlat.filter(m => m.clusterId === cluster.id);
const clusterCosts = clusterModels.map(m => calcCost(m.inputPricePerM, m.outputPricePerM, inTokens, outTokens).totalCost);
const maxCost = Math.max(...clusterCosts);
const scored = clusterModels.map(m => {
const costs = calcCost(m.inputPricePerM, m.outputPricePerM, inTokens, outTokens);
const score = scoreWithinCluster(m, task.type, clusterModels);
const isBest = false;
return {
modelId: m.id, modelName: m.name, provider: m.provider,
clusterId: m.clusterId, clusterName: cluster.name,
inputTokens: inTokens, outputTokens: outTokens,
inputCost: costs.inputCost, outputCost: costs.outputCost, totalCost: costs.totalCost,
latencyScore: LATENCY_SCORE[m.latency] ?? 50,
compositeScore: score,
relativeCostPct: Math.round((costs.totalCost / maxCost) * 100),
_isBest: false,
};
});
scored.sort((a,b) => b.compositeScore - a.compositeScore);
const best = scored.find(m => m.compositeScore >= 65) ?? scored[0];
if (best) best._isBest = true;
scored.forEach(m => {
m.fitBadge = m._isBest ? 'recommended' : m.compositeScore >= 65 ? 'sufficient' : 'overkill';
m.insight = getInsight(m.modelName, cluster.name, cluster.id, task.type, m.compositeScore, m._isBest);
});
clusters[cluster.id] = { clusterId: cluster.id, clusterName: cluster.name, models: scored, best: best??null };
allModels.push(...scored);
}
allModels.sort((a,b) => b.compositeScore - a.compositeScore);
const recommended = allModels.find(m => m.compositeScore >= 80) ?? null;
const selected = allModels.find(m => m.modelId === selectedModelId) ?? allModels[0];
let tip = '';
if (recommended && selected && recommended.modelId !== selected.modelId) {
const saving = selected.totalCost - recommended.totalCost;
if (saving > 0.0001) tip = `Switch to ${recommended.modelName} and save ${formatCost(saving)}.`;
else if (saving < -0.0001) tip = `${selected.modelName} costs ${formatCost(Math.abs(saving))} more than ${recommended.modelName}.`;
} else if (selected) {
tip = `${selected.modelName} is a good fit for this task.`;
}
return { prompt, taskType: task.type, taskConfidence: task.confidence, inputTokens: inTokens, outputTokens: outTokens,
clusters, recommended, selected, allModels, tip, estimatedAt: Date.now() };
}
export function formatCost(n) {
if (n >= 1) return '$' + n.toFixed(2);
if (n >= 0.01) return '$' + n.toFixed(3);
return '$' + n.toFixed(4);
}
// ─── Exports ─────────────────────────────────────────────────────────────────
export function getAllClusterModels() {
return CLUSTERS.flatMap(c => c.models.map(m => ({ ...m, clusterId: c.id, clusterName: c.name })));
}
export function getModelById(id) {
for (const c of CLUSTERS) { const m = c.models.find(m => m.id === id); if (m) return { ...m, clusterId: c.id, clusterName: c.name }; }
return null;
}
export function getClusterById(id) { return CLUSTERS.find(c => c.id === id) ?? null; }
export function getClusterIds() { return CLUSTERS.map(c => c.id); }
// ─── Browser global ─────────────────────────────────────────────────────────
if (typeof window !== 'undefined' && typeof module === 'undefined') {
window.LoomLensEngine = { estimatePrompt, formatCost, estimateInputTokens, detectTaskType,
CLUSTERS, LATENCY_SCORE, CAPABILITY_FIT, TASK_KEYWORDS, OUTPUT_TOKENS,
getAllClusterModels, getModelById, getClusterById, getClusterIds };
}