From 3647174c30098a3a6c9e37d4277e448525fe2b01 Mon Sep 17 00:00:00 2001 From: Paul Kloppers Date: Thu, 14 May 2026 08:23:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(redstone):=20/ai=5Fuse=20=E2=80=94=20OpenR?= =?UTF-8?q?outer=20key=20usage=20report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin-only command. Calls GET https://openrouter.ai/api/v1/key (the REST equivalent of the @openrouter/sdk's apiKeys.getCurrent()) and formats the result: - active model (from the settings table) - tier (🟢 free / 💰 paid) - credit limit + remaining (or "none") - credits spent today / week / month / all-time Plus a note that free-model requests don't draw credits — they're capped separately at 50/day (UTC-midnight reset), liftable to 1000/day with $10 credit. Kept fetch-based like the rest of the OpenRouter integration rather than pulling in the SDK. Co-Authored-By: Claude Opus 4.7 --- bot/bot.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) 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));