cd /news/ai-tools/quota-tavily-instant-quota-tavily-re… · home topics ai-tools article
[ARTICLE · art-126936] src=gist.github.com ↗ pub= topic=ai-tools verified=true sentiment=· neutral

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

A developer published quota-tavily, an OpenCode plugin that answers the /quota-tavily command with an instant Tavily API usage report without consuming an LLM turn. The plugin fetches api.tavily.com/usage with a Bearer key and a 15-second timeout, then injects the result with noReply and ignored flags so it stays out of future model context; because OpenCode hooks cannot mark a command as handled, it aborts processing by throwing an error tagged with a Symbol.for("quota-tavily") marker. It requires Node.js 22, uses only Node builtins, is read-only, and is distributed as a public gist.

by read9 min views2 publishedSep 11, 2026

| | /** | | | * 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. | | | * | | | * 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 | | | * 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 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; |

── more in #ai-tools 4 stories · sorted by recency
── more on @opencode 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/quota-tavily-instant…] indexed:0 read:9min 2026-09-11 ·