# quota-tavily: instant /quota-tavily report for OpenCode

> Source: <https://gist.github.com/cardin/ca901db7f5a1e8e1919885bed6cb4963>
> Published: 2026-09-11 13:29:42+00:00

|  | /** | 
|  | * quota-tavily — instant `/quota-tavily` report, no LLM turn. | 
|  | * | 
|  | * Registers the command from the `config` hook and answers it from | 
|  | * api.tavily.com/usage (GET, Bearer key, 15 s timeout), injecting the text with | 
|  | * `noReply: true` and `ignored: true` so it stays out of future model context. | 
|  | * | 
|  | * OpenCode cannot mark a `command.execute.before` command as handled: hooks only | 
|  | * mutate `output.parts`, and returning runs the command template. The hook | 
|  | * therefore throws an error to abort processing before the LLM runs. That error | 
|  | * carries the Symbol.for("quota-tavily") marker, so the abort is identifiable | 
|  | * rather than being a plain, anonymous failure. | 
|  | * | 
|  | * Loader note: the default export is a V1 plugin module `{ id, server }`. | 
|  | * OpenCode's legacy fallback treats *every* module export as a plugin and | 
|  | * throws "Plugin export is not a function" on non-function exports, so the | 
|  | * default module shape must stay; named exports are then ignored by the loader | 
|  | * and remain safe as test seams. `id` is required for file-based plugins. | 
|  | * | 
|  | * API key (never printed): $TAVILY_API_KEY, else the first whitespace token of | 
|  | * ~/.secrets/tavily-api-key. | 
|  | * | 
|  | * Error handling: no key -> "no API key"; request or abort failure -> "request | 
|  | * failed"; 401 -> "API key rejected"; 429 -> "rate limited"; other non-2xx or | 
|  | * unparseable JSON -> message plus the first 500 body chars. A 2xx with valid | 
|  | * JSON renders the usage report. | 
|  | * | 
|  | * Requires Node.js 22 (oldest non-EOL LTS). Node builtins only; read-only. | 
|  | * | 
|  | * Public gist: https://gist.github.com/cardin/ca901db7f5a1e8e1919885bed6cb4963 | 
|  | * Install/upgrade: | 
|  | * curl -fsSL https://gist.githubusercontent.com/cardin/ca901db7f5a1e8e1919885bed6cb4963/raw/quota-tavily.ts \ | 
|  | * -o ~/.config/opencode/plugins/quota-tavily.ts | 
|  | * Or clone and pull: | 
|  | * gh gist clone ca901db7f5a1e8e1919885bed6cb4963 ~/quota-tavily && git -C ~/quota-tavily pull | 
|  | */ | 
|  | import { readFile } from "node:fs/promises"; | 
|  | import { homedir } from "node:os"; | 
|  | import { join } from "node:path"; | 
|  | import type { Plugin, PluginModule } from "@opencode-ai/plugin"; | 
|  | export const COMMAND_ID = "quota-tavily"; | 
|  | export const COMMAND_DESCRIPTION = "Show Tavily API quota remaining"; | 
|  | export const API_URL = "https://api.tavily.com/usage"; | 
|  | export const KEY_FILE = join(homedir(), ".secrets", "tavily-api-key"); | 
|  | export const REQUEST_TIMEOUT_MS = 15_000; | 
|  | export const MAX_ERROR_BODY_CHARS = 500; | 
|  | export const HANDLED_COMMAND = Symbol.for("quota-tavily"); | 
|  | /** `api.tavily.com/usage`, derived so display text can never drift from API_URL. */ | 
|  | const { host: API_HOST, pathname: API_PATH } = new URL(API_URL); | 
|  | const API_DISPLAY = `${API_HOST}${API_PATH}`; | 
|  | export interface TavilyUsage { | 
|  | key?: { | 
|  | usage?: number \| null; | 
|  | limit?: number \| null; | 
|  | } \| null; | 
|  | account?: { | 
|  | current_plan?: string \| null; | 
|  | plan_usage?: number \| null; | 
|  | plan_limit?: number \| null; | 
|  | } \| null; | 
|  | } | 
|  | /** | 
|  | * Coerce an untrusted value to a finite number, or null when it is not one. | 
|  | * This is the single rule behind every field read below. | 
|  | */ | 
|  | export function toCount(value: unknown): number \| null { | 
|  | return typeof value === "number" && Number.isFinite(value) ? value : null; | 
|  | } | 
|  | /** Render a used/limit pair. */ | 
|  | export function formatCount(used: unknown, limit: unknown): string { | 
|  | const usedCount = toCount(used); | 
|  | const limitCount = toCount(limit); | 
|  | if (usedCount === null && limitCount === null) return "n/a"; | 
|  | if (limitCount === null) return `${usedCount} used / unlimited`; | 
|  | if (usedCount === null) return `${limitCount} limit`; | 
|  | if (limitCount === 0) return `${usedCount} used / 0 limit`; | 
|  | const remaining = limitCount - usedCount; | 
|  | const percentLeft = (remaining / limitCount) * 100; | 
|  | return `${remaining} / ${limitCount} remaining (${percentLeft.toFixed(1)}% left, ${usedCount} used)`; | 
|  | } | 
|  | /** Render a parsed usage payload. */ | 
|  | export function formatUsage(data: TavilyUsage): string { | 
|  | const account = data.account ?? {}; | 
|  | const key = data.key ?? {}; | 
|  | const lines = [`Tavily quota (${API_DISPLAY}):`]; | 
|  | const hasPlanData = toCount(account.plan_usage) !== null \|\| toCount(account.plan_limit) !== null; | 
|  | const hasKeyData = toCount(key.usage) !== null \|\| toCount(key.limit) !== null; | 
|  | if (hasPlanData) { | 
|  | const planName = | 
|  | typeof account.current_plan === "string" && account.current_plan.length > 0 | 
|  | ? ` (${account.current_plan})` | 
|  | : ""; | 
|  | lines.push(`Plan${planName}: ${formatCount(account.plan_usage, account.plan_limit)}`); | 
|  | } else if (hasKeyData) { | 
|  | lines.push(`Key: ${formatCount(key.usage, key.limit)}`); | 
|  | } else { | 
|  | lines.push("No usage data returned."); | 
|  | } | 
|  | return lines.join("\n"); | 
|  | } | 
|  | export function noApiKeyMessage(): string { | 
|  | return [ | 
|  | "Tavily quota: no API key found.", | 
|  | `Set TAVILY_API_KEY or write the key to ${KEY_FILE}.`, | 
|  | ].join("\n"); | 
|  | } | 
|  | export function truncateBody(body: string): string { | 
|  | return body.slice(0, MAX_ERROR_BODY_CHARS); | 
|  | } | 
|  | /** | 
|  | * Turn an HTTP status and raw body into the user-facing report. | 
|  | * Pure: no I/O, so every status branch is directly unit-testable. | 
|  | */ | 
|  | export function renderUsageResponse(status: number, body: string): string { | 
|  | if (status === 401) { | 
|  | return `Tavily quota: API key rejected (401). Check TAVILY_API_KEY or ${KEY_FILE}.`; | 
|  | } | 
|  | if (status === 429) { | 
|  | return "Tavily quota: rate limited on /usage (10 requests / 10 min). Try again later."; | 
|  | } | 
|  | if (status < 200 \|\| status >= 300) { | 
|  | return `Tavily quota: HTTP ${status} from ${API_DISPLAY}\n${truncateBody(body)}`; | 
|  | } | 
|  | let data: TavilyUsage; | 
|  | try { | 
|  | data = JSON.parse(body) as TavilyUsage; | 
|  | } catch { | 
|  | return `Tavily quota: invalid JSON from ${API_DISPLAY}\n${truncateBody(body)}`; | 
|  | } | 
|  | return formatUsage(data); | 
|  | } | 
|  | /** | 
|  | * Resolve the API key from the environment, then the key file. | 
|  | * Parameters are injectable so tests never touch the real environment or file. | 
|  | */ | 
|  | export async function loadApiKey( | 
|  | env: Record<string, string \| undefined> = process.env, | 
|  | keyFile: string = KEY_FILE, | 
|  | ): Promise<string> { | 
|  | const fromEnv = (env.TAVILY_API_KEY ?? "").trim(); | 
|  | if (fromEnv) return fromEnv; | 
|  | try { | 
|  | const contents = await readFile(keyFile, "utf8"); | 
|  | return contents.trim().split(/\s+/)[0] ?? ""; | 
|  | } catch { | 
|  | return ""; | 
|  | } | 
|  | } | 
|  | /** | 
|  | * Fetch and render the Tavily usage report. The key loader is injectable so | 
|  | * the no-key branch (and error branches) can be tested offline. | 
|  | */ | 
|  | export async function fetchUsage( | 
|  | loadKey: () => Promise<string> = loadApiKey, | 
|  | ): Promise<string> { | 
|  | const key = await loadKey(); | 
|  | if (!key) return noApiKeyMessage(); | 
|  | let response: Response; | 
|  | let body: string; | 
|  | try { | 
|  | response = await fetch(API_URL, { | 
|  | headers: { Authorization: `Bearer ${key}` }, | 
|  | signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | 
|  | }); | 
|  | body = await response.text().catch(() => ""); | 
|  | } catch (error) { | 
|  | const reason = error instanceof Error ? error.message : String(error); | 
|  | return `Tavily quota: request failed (${reason}). Check your network.`; | 
|  | } | 
|  | return renderUsageResponse(response.status, body); | 
|  | } | 
|  | /** Abort command processing, tagging the thrown error with the sentinel. */ | 
|  | function abortHandledCommand(): never { | 
|  | const error = new Error("quota-tavily: command handled, output already injected"); | 
|  | error.name = "QuotaTavilyHandled"; | 
|  | Object.defineProperty(error, HANDLED_COMMAND, { value: true }); | 
|  | throw error; | 
|  | } | 
|  | export const TavilyQuota: Plugin = async ({ client }) => { | 
|  | /** Inject text without triggering a model reply or entering future context. */ | 
|  | async function injectOutput(sessionID: string, text: string): Promise<void> { | 
|  | try { | 
|  | await client.session.prompt({ | 
|  | path: { id: sessionID }, | 
|  | body: { | 
|  | noReply: true, | 
|  | parts: [{ type: "text", text, ignored: true }], | 
|  | }, | 
|  | }); | 
|  | } catch (error) { | 
|  | await client.app.log({ | 
|  | body: { | 
|  | service: "quota-tavily", | 
|  | level: "warn", | 
|  | message: "Failed to inject quota output", | 
|  | extra: { error: error instanceof Error ? error.message : String(error) }, | 
|  | }, | 
|  | }); | 
|  | throw error; | 
|  | } | 
|  | } | 
|  | async function handleCommand(input: { command: string; sessionID: string }): Promise<void> { | 
|  | const isTarget = input.command.replace(/^\//, "") === COMMAND_ID; | 
|  | if (!isTarget \|\| !input.sessionID) return; | 
|  | await injectOutput(input.sessionID, await fetchUsage()); | 
|  | abortHandledCommand(); | 
|  | } | 
|  | return { | 
|  | config: async (config) => { | 
|  | config.command ??= {}; | 
|  | config.command[COMMAND_ID] ??= { | 
|  | template: `/${COMMAND_ID}`, | 
|  | description: COMMAND_DESCRIPTION, | 
|  | }; | 
|  | }, | 
|  | "command.execute.before": handleCommand, | 
|  | }; | 
|  | }; | 
|  | export default { | 
|  | id: "quota-tavily", | 
|  | server: TavilyQuota, | 
|  | } satisfies PluginModule; |
