Spaces:
Running
Running
Aster Vale
update: Per-Job pricing to $0.033/estimate — 1,000 jobs = $33 vs $15 Pro subscription
2dee62e Download loomlens-engine.js from Signalloom-ai/LoomLensAI: direct link, hf CLI and curl.
- Browser
- Download file 13.3 kB
-
https://huggingface.co/spaces/Signalloom-ai/LoomLensAI/resolve/main/loomlens-engine.js
- Command line
-
hf download hf://spaces/Signalloom-ai/LoomLensAI/loomlens-engine.js
-
curl -L -o loomlens-engine.js https://huggingface.co/spaces/Signalloom-ai/LoomLensAI/resolve/main/loomlens-engine.js
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 }; | |
| } | |