diff --git a/bot/bot.ts b/bot/bot.ts index 6d2e5d5..7916162 100644 --- a/bot/bot.ts +++ b/bot/bot.ts @@ -626,6 +626,62 @@ async function fetchOpenRouterModels(): Promise { return data.data ?? []; } +// OpenRouter key/usage info — GET /api/v1/key (equivalent to the SDK's +// openRouter.apiKeys.getCurrent()). Surfaces credit usage + tier. +type ORKeyInfo = { + label?: string; + is_free_tier?: boolean; + limit?: number | null; + limit_remaining?: number | null; + usage?: number; + usage_daily?: number; + usage_weekly?: number; + usage_monthly?: number; +}; + +async function fetchOpenRouterKeyInfo(): Promise { + if (!OPENROUTER_API_KEY) throw new Error("OPENROUTER_API_KEY not configured"); + const res = await fetch("https://openrouter.ai/api/v1/key", { + headers: { Authorization: `Bearer ${OPENROUTER_API_KEY}`, Accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + if (!res.ok) throw new Error(`openrouter /key -> ${res.status}`); + const data = (await res.json()) as { data?: ORKeyInfo }; + return data.data ?? {}; +} + +async function renderAiUsage(ctx: Context) { + if (!OPENROUTER_API_KEY) { + await sendText(ctx, "OpenRouter is not configured — set OPENROUTER_API_KEY to see usage."); + return; + } + let info: ORKeyInfo; + try { + info = await fetchOpenRouterKeyInfo(); + } catch (e) { + await sendText(ctx, `Couldn't load OpenRouter usage: ${escape((e as Error).message)}`); + return; + } + const usd = (n: number | null | undefined) => + n == null ? "—" : `$${n.toFixed(n < 1 ? 4 : 2)}`; + const tier = info.is_free_tier ? "🟢 free tier" : "💰 paid"; + const limitLine = info.limit == null + ? "Credit limit: none" + : `Credit limit: ${usd(info.limit)} (remaining ${usd(info.limit_remaining)})`; + const text = + `🤖 OpenRouter usage\n\n` + + `Active model: ${escape(currentOpenRouterModel())}\n` + + `Tier: ${tier}\n` + + `${limitLine}\n\n` + + `Credits spent\n` + + `• today: ${usd(info.usage_daily)}\n` + + `• this week: ${usd(info.usage_weekly)}\n` + + `• this month: ${usd(info.usage_monthly)}\n` + + `• all time: ${usd(info.usage)}\n\n` + + `Free-model requests don't consume credits — they're capped separately at 50/day, resetting at UTC midnight. Add $10 of credit to lift that to 1000/day.`; + await sendText(ctx, text); +} + function modelButtonLabel(m: ORModelListItem, isCurrent: boolean): string { const free = isFreeModel(m); const thinking = m.supported_parameters?.includes("reasoning") ?? false; @@ -1104,6 +1160,9 @@ bot.command("teststicker", admin(async (ctx) => { // is resolved at every callOpenRouterOnce so changes apply immediately. bot.command("model", admin(async (ctx) => { await renderModelPicker(ctx); })); +// OpenRouter usage — admin views credit spend + tier for the configured key. +bot.command("ai_use", admin(async (ctx) => { await renderAiUsage(ctx); })); + // History — last N messages in this chat bot.command("history", admin(async (ctx) => { const n = Math.min(50, Math.max(5, Number.parseInt((ctx.match as string).trim(), 10) || 20));