Eddlm

Nicer Ollama Cloud usage breakdown

Aug 29th, 2026 (edited)
42
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 8.82 KB | Source Code | 0 0
  1. // ==UserScript==
  2. // @name         Ollama Cloud Usage Stats
  3. // @namespace    https://ollama.com/
  4. // @version      2.1.0
  5. // @description  Extends the Ollama Cloud Usage dashboard: merges the weekly % into each model's request count and adds a per-1k-requests cost column (weekly table edited in place; same kind of table injected below the session bar).
  6. // @author       you
  7. // @match        https://ollama.com/settings
  8. // @match        https://ollama.com/settings/*
  9. // @match        https://ollama.com/settings?*
  10. // @run-at       document-idle
  11. // @grant        none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15.   'use strict';
  16.  
  17.   console.log('[ollama-stats] userscript loaded');
  18.  
  19.   // --- Shared column styles (header + value share widths/alignment) ---------
  20.   const COL_USAGE =
  21.     'flex:none;min-width:6.5rem;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;';
  22.   const COL_PER1K =
  23.     'flex:none;min-width:4rem;text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;padding-left:.75rem;';
  24.  
  25.   // --- Track helpers ---------------------------------------------------------
  26.   function trackBy(kind) {
  27.     return Array.from(document.querySelectorAll('[data-usage-track]')).find(
  28.       (t) => (t.getAttribute('aria-label') || '').toLowerCase().includes(kind)
  29.     );
  30.   }
  31.  
  32.   function segmentsOf(track) {
  33.     if (!track) return null;
  34.     const segs = Array.from(track.querySelectorAll('[data-usage-segment]'))
  35.       .map((seg) => ({
  36.         model: seg.dataset.model,
  37.         requests: parseInt(seg.dataset.requests, 10) || 0,
  38.         width: parseFloat((seg.style.width || '0').replace('%', '')) || 0,
  39.         color: seg.style.background || '',
  40.       }))
  41.       .filter((s) => s.model);
  42.     return segs.length ? segs : null;
  43.   }
  44.  
  45.   // --- Derive the overall "used %" from the meter label --------------------
  46.   function usedOf(track) {
  47.     if (!track) return null;
  48.     const m = (track.getAttribute('aria-label') || '').match(
  49.       /([\d.]+)%\s*used/i
  50.     );
  51.     if (m) return parseFloat(m[1]);
  52.     const inner = track.querySelector('[class*="bg-neutral-950"]');
  53.     if (inner && inner.style.width) return parseFloat(inner.style.width) || null;
  54.     return null;
  55.   }
  56.  
  57.   // --- Compute stats ---------------------------------------------------------
  58.   function computeStats(segs, used) {
  59.     const usedPct = used != null ? used : 0;
  60.     const rows = segs.map((s) => {
  61.       const contrib = (s.width / 100) * usedPct;
  62.       return {
  63.         model: s.model,
  64.         requests: s.requests,
  65.         color: s.color,
  66.         contrib,
  67.         // Per-1000-request contribution (extrapolated): how much would 1,000
  68.         // of these requests consume of the meter's limit?
  69.         per1k: s.requests > 0 ? (contrib / s.requests) * 1000 : 0,
  70.       };
  71.     });
  72.     rows.sort((a, b) => b.per1k - a.per1k);
  73.     return { rows, used: usedPct };
  74.   }
  75.  
  76.   // --- Shared table header ---------------------------------------------------
  77.   function headerHTML(title) {
  78.     return (
  79.       '<span aria-hidden="true" style="flex:0 0 auto;width:.5rem;height:.5rem;"></span>' +
  80.       '<span class="min-w-0 flex-1" style="color:#a3a3a3;">' + title + '</span>' +
  81.       '<span class="flex-none" style="' + COL_USAGE + 'color:#a3a3a3;">Usage</span>' +
  82.       '<span class="flex-none" style="' + COL_PER1K + 'color:#a3a3a3;">per 1k</span>'
  83.     );
  84.   }
  85.  
  86.   // --- Weekly table: edit the original list in place ------------------------
  87.   function renderWeekly(stats) {
  88.     const list = document.querySelector('#weekly-usage-models');
  89.     if (!list) return;
  90.  
  91.     const byModel = new Map(stats.rows.map((r) => [r.model, r]));
  92.     const rows = Array.from(list.querySelectorAll(':scope > div.flex'))
  93.       .map((row) => ({
  94.         row,
  95.         r: byModel.get(row.querySelector('span[title]')?.getAttribute('title')),
  96.       }))
  97.       .filter((x) => x.r);
  98.     if (!rows.length) return;
  99.  
  100.     rows.forEach(({ row, r }) => {
  101.       // Merge the weekly % into the existing "N requests" text (true black).
  102.       const reqSpan = row.querySelector('span.tabular-nums');
  103.       if (reqSpan && !reqSpan.dataset.ossMerged) {
  104.         reqSpan.dataset.ossMerged = '1';
  105.         reqSpan.style.cssText = COL_USAGE + 'color:#a3a3a3;';
  106.         reqSpan.innerHTML =
  107.           reqSpan.textContent.trim() +
  108.           ' (<span style="color:#111;">' + Math.round(r.contrib) + '%</span>)';
  109.       }
  110.       // Append the per-1k-requests column.
  111.       if (!row.querySelector('[data-oss="pr"]')) {
  112.         const pr = document.createElement('span');
  113.         pr.dataset.oss = 'pr';
  114.         pr.style.cssText = COL_PER1K + 'color:#404040;';
  115.         pr.textContent = r.per1k.toFixed(1) + '%';
  116.         row.appendChild(pr);
  117.       }
  118.     });
  119.  
  120.     // Header: reuse the original "Models used this week" label div.
  121.     const label = Array.from(list.children).find(
  122.       (el) => el.tagName === 'DIV' && !el.querySelector('span[title]')
  123.     );
  124.     if (label) {
  125.       label.className = 'flex min-w-0 items-center gap-2 text-xs';
  126.       label.innerHTML = headerHTML('Models used this week');
  127.     } else if (!document.getElementById('oss-header')) {
  128.       const header = document.createElement('div');
  129.       header.id = 'oss-header';
  130.       header.className = 'flex min-w-0 items-center gap-2 text-xs';
  131.       header.innerHTML = headerHTML('Models used this week');
  132.       list.insertBefore(header, list.firstElementChild);
  133.     }
  134.  
  135.     // Reorder rows by per-1k cost (highest first).
  136.     rows.sort((a, b) => b.r.per1k - a.r.per1k);
  137.     rows.forEach(({ row }) => list.appendChild(row));
  138.     console.log('[ollama-stats] weekly list updated');
  139.   }
  140.  
  141.   // --- Session table: injected right below the session bar ------------------
  142.   function renderSession(stats) {
  143.     const track = trackBy('session');
  144.     if (!track) return;
  145.     const meter = track.closest('[data-usage-meter]') || track.parentElement;
  146.     if (!meter || !meter.parentElement) return;
  147.  
  148.     let list = document.getElementById('session-usage-models');
  149.     if (!list) {
  150.       list = document.createElement('div');
  151.       list.id = 'session-usage-models';
  152.       list.className = 'mt-3 space-y-1.5';
  153.       meter.insertAdjacentElement('afterend', list);
  154.     }
  155.     list.innerHTML = '';
  156.  
  157.     const header = document.createElement('div');
  158.     header.className = 'flex min-w-0 items-center gap-2 text-xs';
  159.     header.innerHTML = headerHTML('Models used this session');
  160.     list.appendChild(header);
  161.  
  162.     stats.rows.forEach((r) => {
  163.       const row = document.createElement('div');
  164.       row.className = 'flex min-w-0 items-center gap-2 text-xs';
  165.  
  166.       const dot = document.createElement('span');
  167.       dot.className = 'h-2 w-2 flex-none rounded-sm';
  168.       dot.setAttribute('aria-hidden', 'true');
  169.       dot.style.background = r.color || '#d4d4d4';
  170.  
  171.       const name = document.createElement('span');
  172.       name.className = 'min-w-0 flex-1 truncate text-neutral-700';
  173.       name.title = r.model;
  174.       name.textContent = r.model;
  175.  
  176.       const req = document.createElement('span');
  177.       req.style.cssText = COL_USAGE + 'color:#a3a3a3;';
  178.       req.innerHTML =
  179.         r.requests + (r.requests === 1 ? ' request' : ' requests') +
  180.         ' (<span style="color:#111;">' + Math.round(r.contrib) + '%</span>)';
  181.  
  182.       const pr = document.createElement('span');
  183.       pr.style.cssText = COL_PER1K + 'color:#404040;';
  184.       pr.textContent = r.per1k.toFixed(1) + '%';
  185.  
  186.       row.append(dot, name, req, pr);
  187.       list.appendChild(row);
  188.     });
  189.  
  190.     console.log('[ollama-stats] session table rendered');
  191.   }
  192.  
  193.   // --- Orchestration ---------------------------------------------------------
  194.   function tryRun() {
  195.     const wTrack = trackBy('weekly');
  196.     if (!wTrack) return false;
  197.     const wSegs = segmentsOf(wTrack);
  198.     if (!wSegs) return false;
  199.  
  200.     renderWeekly(computeStats(wSegs, usedOf(wTrack)));
  201.  
  202.     const sTrack = trackBy('session');
  203.     const sSegs = sTrack && segmentsOf(sTrack);
  204.     if (sSegs) renderSession(computeStats(sSegs, usedOf(sTrack)));
  205.     return true;
  206.   }
  207.  
  208.   if (tryRun()) return;
  209.  
  210.   // Content loads asynchronously — observe for the bars to appear.
  211.   const observer = new MutationObserver(() => {
  212.     if (tryRun()) observer.disconnect();
  213.   });
  214.   observer.observe(document.documentElement, { childList: true, subtree: true });
  215.  
  216.   // Safety fallback: also poll for a few seconds.
  217.   let attempts = 0;
  218.   const poll = setInterval(() => {
  219.     if (tryRun() || ++attempts > 30) {
  220.       clearInterval(poll);
  221.       if (!document.querySelector('#weekly-usage-models')) {
  222.         console.warn(
  223.           '[ollama-stats] never found segments — check that you are on ollama.com/settings'
  224.         );
  225.       }
  226.     }
  227.   }, 500);
  228.  
  229.   // Re-render if htmx swaps the page in-place.
  230.   document.addEventListener('htmx:afterSwap', () => {
  231.     document.getElementById('session-usage-models')?.remove();
  232.     tryRun();
  233.   });
  234. })();
  235.  
Advertisement
Add Comment
Please, Sign In to add comment