Skip to content

Instantly share code, notes, and snippets.

@MeirionHughes
Created August 29, 2026 19:55
Show Gist options
  • Select an option

  • Save MeirionHughes/d87161f8b29b24c7fa96e56f571e7045 to your computer and use it in GitHub Desktop.

Select an option

Save MeirionHughes/d87161f8b29b24c7fa96e56f571e7045 to your computer and use it in GitHub Desktop.
opencode tui plugin: shows normalised estimate of MONTHLY usage allowance of currently available models. scraps the go whensite, so future site changes may break this.
/** @jsxImportSource @opentui/solid */
import { useKeyboard } from "@opentui/solid";
import { TextAttributes } from "@opentui/core";
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { createSignal } from "solid-js";
import type { TuiPlugin, TuiPluginApi } from "@opencode-ai/plugin/tui";
type Theme = TuiPluginApi["theme"]["current"];
type BaselineMode = "fixed" | "estimated";
type RequestPattern = {
input: number;
cache: number;
output: number;
};
type Row = {
model: string;
key: string;
usage: number;
rawMonthly: number;
normalized: number;
isNew?: boolean;
changed?: boolean;
multiplier?: number;
};
type ParsedModel = {
model: string;
key: string;
usage: number;
inputP: number;
outputP: number;
cachedP: number;
estimated: RequestPattern;
multiplier: number;
};
type SessionData = {
fixed: Row[];
estimated: Row[];
};
const MEM_DIR = join(process.env.LOCALAPPDATA, "opencode");
const MEM_PATH = join(MEM_DIR, "go-usage-memory.json");
type StoredValue = number | "inf";
type StoredEntry = Partial<Record<BaselineMode, StoredValue>>;
function loadKnown(): Map<string, StoredEntry> {
try {
if (!existsSync(MEM_PATH)) return new Map();
const parsed = JSON.parse(readFileSync(MEM_PATH, "utf8"));
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
const map = new Map<string, StoredEntry>();
const storedModels =
parsed.models && typeof parsed.models === "object" ? parsed.models : parsed;
for (const [k, v] of Object.entries(storedModels)) {
if ((typeof v === "number" && Number.isFinite(v)) || v === "inf") {
map.set(k, { fixed: v });
continue;
}
if (v && typeof v === "object" && !Array.isArray(v)) {
const entry: StoredEntry = {};
for (const mode of ["fixed", "estimated"] as const) {
const value = (v as Record<string, unknown>)[mode];
if ((typeof value === "number" && Number.isFinite(value)) || value === "inf") {
entry[mode] = value;
}
}
if (entry.fixed !== undefined || entry.estimated !== undefined) {
map.set(k, entry);
}
}
}
return map;
}
} catch {
/* ignore corrupt memory */
}
return new Map();
}
function saveKnown(values: Record<string, StoredEntry>): void {
try {
mkdirSync(MEM_DIR, { recursive: true });
writeFileSync(MEM_PATH, JSON.stringify(values, null, 2), "utf8");
} catch {
/* ignore write failures */
}
}
const URL = "https://opencode.ai/docs/go/";
const GRAPH_URL = "https://opencode.ai/go";
const SCROLL_HEIGHT = 22;
const SEP = "─".repeat(64);
const MODEL_WIDTH = 17;
const VALUE_WIDTH = 4;
const GRAPH_WIDTH = 24;
const LOG_MIN = 10;
const LOG_MAX = 1_000_000;
const DECADES: { label: string; color: string }[] = [
{ label: "10s", color: "#00aa00" },
{ label: "100s", color: "#00cccc" },
{ label: "1k", color: "#cc66cc" },
{ label: "10k", color: "#ccaa00" },
{ label: "100k", color: "#cc4444" },
{ label: "1M+", color: "#ffffff" },
];
const DECADE_CELLS = [1, 2, 3, 4, 5, 6]; // cells per decade
function decodeEntities(s: string): string {
return s
.replace(/&nbsp;/g, " ")
.replace(/&mdash;/g, "—")
.replace(/&ndash;/g, "–")
.replace(/&amp;/g, "&")
.replace(/&#0*8212;/g, "—")
.replace(/&#0*8211;/g, "–")
.replace(/&#0*38;/g, "&")
.replace(/&[a-z]+;/gi, " ");
}
function stripTags(s: string): string {
return s.replace(/<[^>]+>/g, " ");
}
function expandAssumptionNames(raw: string): string[] {
const parts = raw.split("/").map((part) => part.trim()).filter(Boolean);
if (parts.length <= 1) return parts;
const first = parts[0];
const firstSpace = first.lastIndexOf(" ");
const firstDigit = first.search(/\d/);
const wordPrefix = firstSpace >= 0 ? first.slice(0, firstSpace + 1) : "";
const digitPrefix = firstDigit >= 0 ? first.slice(0, firstDigit) : first;
return parts.map((part, i) => {
if (i === 0) return part;
if (/^\d/.test(part)) return `${digitPrefix}${part}`;
if (/^[A-Za-z]?\d/.test(part)) return `${wordPrefix}${part}`;
return part;
});
}
function parseAssumptions(html: string): Map<string, RequestPattern> {
const assumptions = new Map<string, RequestPattern>();
const items = [...html.matchAll(/<li[^>]*>([\s\S]*?)<\/li>/gi)];
for (const item of items) {
const text = decodeEntities(stripTags(item[1])).replace(/\s+/g, " ").trim();
const match = text.match(
/^(.+?)\s+[-]\s*([\d,]+)\s+input,\s*([\d,]+)\s+cached,\s*([\d,]+)\s+output\s+tokens?\s+per\s+request$/i,
);
if (!match) continue;
const pattern = {
input: Number(match[2].replace(/,/g, "")),
cache: Number(match[3].replace(/,/g, "")),
output: Number(match[4].replace(/,/g, "")),
};
for (const name of expandAssumptionNames(match[1])) {
assumptions.set(norm(name), pattern);
}
}
return assumptions;
}
function parseBonuses(html: string): Map<string, number> {
const bonuses = new Map<string, number>();
const pills = html.match(/<div data-slot="pills">([\s\S]*?)<\/div>/);
if (!pills) return bonuses;
const items = [
...pills[1].matchAll(/data-model="([^"]+)"[^>]*>([\s\S]*?)(?=data-model="|$)/g),
];
for (const item of items) {
const model = norm(item[1]);
const inner = item[2];
const m = inner.match(
/data-bonus>\s*([\d.]+)\s*x\s*(?:\([^)]*\)\s*)?usage/i,
);
if (m) bonuses.set(model, Number(m[1]));
}
return bonuses;
}
function norm(s: string): string {
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
}
function abbreviateModel(name: string): string {
if (name.length <= 10) return name;
const words = name.split(/\s+/).filter(Boolean);
let pos = 0;
const tokens: string[] = [];
let abbrev = "";
for (const w of words) {
const keepFull = pos <= 10 || /^\d/.test(w);
if (keepFull) {
if (abbrev) {
tokens.push(abbrev);
abbrev = "";
}
tokens.push(w);
} else {
abbrev += w[0];
}
pos += w.length + 1;
}
if (abbrev) tokens.push(abbrev);
return tokens.join(" ");
}
function num(s: string | undefined): number | null {
if (s == null) return null;
const t = String(s).replace(/[$,\s]/g, "");
if (t === "-" || t === "") return null;
const v = parseFloat(t);
return Number.isFinite(v) ? v : null;
}
// Fixed assumed request composition (alternative to observed per-model patterns).
const TOK_INPUT = 1000;
const TOK_CACHE = 80000;
const TOK_OUTPUT = 500;
const FIXED_PATTERN: RequestPattern = {
input: TOK_INPUT,
cache: TOK_CACHE,
output: TOK_OUTPUT,
};
function formatValue(value: number): string {
if (!Number.isFinite(value)) return "∞";
const rounded = Math.max(0, Math.round(value));
if (rounded >= 1_000_000) return `${Math.round(rounded / 1_000_000)}m`;
if (rounded >= 1_000) {
const thousands = rounded / 1_000;
if (thousands < 10) return `${thousands.toFixed(1)}k`;
const compactThousands = Math.round(thousands);
return compactThousands >= 1_000 ? "1m" : `${compactThousands}k`;
}
return String(rounded);
}
function logPosition(value: number): number {
const clamped = Math.min(LOG_MAX, Math.max(LOG_MIN, value));
const ratio =
(Math.log10(clamped) - Math.log10(LOG_MIN)) /
(Math.log10(LOG_MAX) - Math.log10(LOG_MIN));
return Math.round(ratio * (GRAPH_WIDTH - 1));
}
function decadeFor(value: number): number {
if (!Number.isFinite(value) || value >= LOG_MAX) return 5;
return Math.min(
4,
Math.max(0, Math.floor(Math.log10(Math.max(LOG_MIN, value))) - 1),
);
}
function parseTable(html: string): string[][] {
const tables = [...html.matchAll(/<table[^>]*>([\s\S]*?)<\/table>/g)].map((m) => m[0]);
const table = tables.find(
(t) => /<th[^>]*>Usage/i.test(t) && /Cached Read/i.test(t),
);
if (!table) throw new Error("usage table not found");
const rows = [...table.matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/g)].map((m) => m[1]);
return rows
.filter((r) => !/<th/i.test(r))
.map((r) =>
[...r.matchAll(/<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/g)].map((c) =>
stripTags(decodeEntities(c[1])).trim(),
),
);
}
async function buildRows(): Promise<{ models: ParsedModel[]; error?: string }> {
let html: string;
try {
const res = await fetch(URL);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
html = await res.text();
} catch (e) {
return { models: [], error: String(e) };
}
try {
const rows = parseTable(html);
const assumptions = parseAssumptions(html);
let bonuses: Map<string, number> = new Map();
try {
const gres = await fetch(GRAPH_URL);
if (gres.ok) bonuses = parseBonuses(await gres.text());
} catch {
/* graph bonuses are optional */
}
const models: ParsedModel[] = [];
const seen = new Set<string>();
for (const cells of rows) {
const model = cells[0];
if (!model) continue;
const base = norm(model.replace(/\(.*?\)/g, "").trim());
if (seen.has(base)) continue; // dedupe (e.g. Grok 4.6 x2)
seen.add(base);
const inputP = num(cells[1]);
const outputP = num(cells[2]);
const cachedP = num(cells[3]);
const usage = num(cells[5]);
if (inputP == null || outputP == null || cachedP == null || usage == null)
continue;
models.push({
model: model.replace(/\(.*?\)/g, "").trim(),
key: base,
usage,
inputP,
outputP,
cachedP,
estimated: assumptions.get(base) ?? FIXED_PATTERN,
multiplier: bonuses.get(base) ?? 1,
});
}
return { models };
} catch (e) {
return { models: [], error: String(e) };
}
}
function calculateRows(models: ParsedModel[], mode: BaselineMode): Row[] {
const rows = models.map((model) => {
const pattern = mode === "estimated" ? model.estimated : FIXED_PATTERN;
const costPerRequest =
(pattern.input / 1e6) * model.inputP +
(pattern.cache / 1e6) * model.cachedP +
(pattern.output / 1e6) * model.outputP;
const rawMonthly =
costPerRequest === 0 ? Infinity : (model.usage / costPerRequest) * model.multiplier;
const factor = model.usage / 60; // $60 => 1.0; $30 => 0.5; $15 => 0.25
const normalized = costPerRequest === 0 ? Infinity : rawMonthly * factor;
return {
model: model.model,
key: model.key,
usage: model.usage,
rawMonthly,
normalized,
multiplier: model.multiplier,
};
});
rows.sort((a, b) => b.normalized - a.normalized);
return rows;
}
function markRows(
rows: Row[],
known: Map<string, StoredEntry>,
mode: BaselineMode,
): void {
for (const row of rows) {
const previous = known.get(row.key)?.[mode];
row.isNew = known.get(row.key) === undefined;
row.changed =
previous !== undefined &&
(row.normalized === Infinity
? previous !== "inf"
: previous === "inf" || Math.round(previous) !== Math.round(row.normalized));
}
}
function storedValue(value: number): StoredValue {
return Number.isFinite(value) ? value : "inf";
}
function GoUsageView(props: {
theme: Theme;
rows?: Row[];
estimatedRows?: Row[];
loading?: boolean;
error?: string;
}) {
const theme = props.theme;
const [mode, setMode] = createSignal<BaselineMode>("fixed");
const displayedRows = () =>
mode() === "estimated" ? props.estimatedRows ?? props.rows : props.rows;
let scroll: { scrollHeight: number; height: number; scrollBy(d: number): void } | undefined;
useKeyboard((evt: any) => {
if (String(evt.name).toLowerCase() === "m") {
setMode(mode() === "fixed" ? "estimated" : "fixed");
evt.preventDefault();
evt.stopPropagation();
return;
}
if (!scroll) return;
if (scroll.scrollHeight <= scroll.height) return;
if (evt.name === "up") {
scroll.scrollBy(-1);
evt.preventDefault();
evt.stopPropagation();
} else if (evt.name === "down") {
scroll.scrollBy(1);
evt.preventDefault();
evt.stopPropagation();
} else if (evt.name === "pageup") {
scroll.scrollBy(-10);
evt.preventDefault();
evt.stopPropagation();
} else if (evt.name === "pagedown") {
scroll.scrollBy(10);
evt.preventDefault();
evt.stopPropagation();
}
});
return (
<box
flexDirection="column"
width="100%"
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
gap={0}
>
<box flexDirection="row" width="100%" justifyContent="space-between">
<text fg={theme.primary}>
<b>Go usages limits</b>{" "}
<b>
<span style={{ fg: theme.textMuted }}>
· {mode() === "fixed" ? "$60 normalized baseline" : "site usage baseline"}
</span>
</b>
</text>
<text fg={theme.textMuted}> ↑/↓ · esc</text>
</box>
<text fg={theme.borderSubtle} wrapMode="none">
{SEP}
</text>
{props.loading ? (
<text fg={theme.textMuted} wrapMode="none">
Loading OpenCode Go usage limits from opencode.ai…
</text>
) : props.error ? (
<text fg={theme.error} wrapMode="none">
Failed to load: {props.error}
</text>
) : (
<scrollbox
height={SCROLL_HEIGHT}
scrollY
scrollbarOptions={{ trackOptions: { backgroundColor: theme.borderSubtle } }}
viewportOptions={{ paddingRight: 2 }}
ref={(r: any) => {
scroll = r;
}}
>
<box flexDirection="column" width="100%" gap={0}>
{(displayedRows() ?? []).map((r) => {
const position = logPosition(r.normalized);
const markerColor = DECADES[decadeFor(r.normalized)].color;
const basePosition =
r.multiplier && r.multiplier !== 1
? logPosition(r.normalized / r.multiplier)
: position;
const graph = Array.from({ length: GRAPH_WIDTH }, (_, i) => {
if (i > position) return { char: "─", color: theme.borderSubtle };
if (i <= basePosition)
return { char: "█", color: markerColor };
return { char: "▒", color: markerColor };
});
const abbreviated =
r.model.length > 10 ? abbreviateModel(r.model) : r.model;
const label =
abbreviated.length > MODEL_WIDTH
? abbreviated.slice(0, MODEL_WIDTH - 1) + "…"
: abbreviated;
const right = formatValue(r.normalized).padStart(VALUE_WIDTH);
return (
<text fg={theme.text} wrapMode="none">
{label.padEnd(MODEL_WIDTH)}{" "}
{graph.map((cell) => (
<b>
<span style={{ fg: cell.color }}>{cell.char}</span>
</b>
))}
{r.isNew ? (
<>
{" "}
<b>
<span style={{ fg: "#33ff66" }}>new</span>
</b>
</>
) : r.changed ? (
<>
{" "}
<b>
<span style={{ fg: "#ffcc33" }}>changed</span>
</b>
</>
) : null}
{r.multiplier && r.multiplier !== 1 ? (
<>
{" "}
<b>
<span style={{ fg: "#00cccc" }}>{r.multiplier}x</span>
</b>
</>
) : null}
{" "}
<b>
<span style={{ fg: theme.textMuted }}>{right}</span>
</b>
</text>
);
})}
</box>
</scrollbox>
)}
<text fg={theme.textMuted} wrapMode="none">
{DECADES.map((d, i) => (
<>
<b>
<span style={{ fg: d.color }}>{"█"}</span>
</b>
<b>
<span style={{ fg: theme.textMuted }}>{` ${d.label}`}</span>
</b>
{i < DECADES.length - 1 ? " " : ""}
</>
))}
</text>
<text
fg={theme.borderSubtle}
wrapMode="none"
style={{ attributes: TextAttributes.ITALIC }}
>
{mode() === "fixed"
? "1000 in / 80000 cache / 500 out · m = site estimates"
: "site estimates per model · m = fixed baseline"}
</text>
</box>
);
}
let inFlight = false;
let sessionCache: SessionData | null = null;
async function showGoUsage(api: TuiPluginApi): Promise<void> {
if (inFlight) return;
if (sessionCache) {
api.ui.dialog.setSize("xlarge");
api.ui.dialog.replace(() => (
<GoUsageView
theme={api.theme.current}
rows={sessionCache.fixed}
estimatedRows={sessionCache.estimated}
/>
));
return;
}
inFlight = true;
try {
api.ui.dialog.setSize("xlarge");
api.ui.dialog.replace(() => <GoUsageView theme={api.theme.current} loading />);
const { models, error } = await buildRows();
if (api.lifecycle.signal.aborted) return;
if (error || models.length === 0) {
api.ui.dialog.replace(() => (
<GoUsageView theme={api.theme.current} error={error ?? "no models parsed"} />
));
return;
}
const known = loadKnown();
const fixed = calculateRows(models, "fixed");
const estimated = calculateRows(models, "estimated");
markRows(fixed, known, "fixed");
markRows(estimated, known, "estimated");
const store: Record<string, StoredEntry> = {};
for (const model of models) {
const fixedRow = fixed.find((row) => row.key === model.key);
const estimatedRow = estimated.find((row) => row.key === model.key);
if (fixedRow && estimatedRow) {
store[model.key] = {
fixed: storedValue(fixedRow.normalized),
estimated: storedValue(estimatedRow.normalized),
};
}
}
saveKnown(store);
sessionCache = { fixed, estimated };
api.ui.dialog.replace(() => (
<GoUsageView theme={api.theme.current} rows={fixed} estimatedRows={estimated} />
));
} catch (e) {
api.ui.toast({
title: "Go usages limits",
message: String(e),
variant: "error",
});
} finally {
inFlight = false;
}
}
const tui: TuiPlugin = async (api) => {
api.keymap.registerLayer({
commands: [
{
name: "go-usage-limits.show",
title: "Go usages limits",
category: "Plugin",
namespace: "palette",
slashName: "go-usage-limits",
run: () => {
void showGoUsage(api);
},
},
],
});
};
const plugin = { id: "go-usage-limits", tui };
export default plugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment