feat(redstone): /ai_use — OpenRouter key usage report

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 08:23:08 +02:00
parent 633a4443c2
commit 3647174c30

View File

@@ -626,6 +626,62 @@ async function fetchOpenRouterModels(): Promise<ORModelListItem[]> {
return data.data ?? []; 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<ORKeyInfo> {
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: <i>none</i>"
: `Credit limit: ${usd(info.limit)} (remaining ${usd(info.limit_remaining)})`;
const text =
`<b>🤖 OpenRouter usage</b>\n\n` +
`Active model: <code>${escape(currentOpenRouterModel())}</code>\n` +
`Tier: ${tier}\n` +
`${limitLine}\n\n` +
`<b>Credits spent</b>\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` +
`<i>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.</i>`;
await sendText(ctx, text);
}
function modelButtonLabel(m: ORModelListItem, isCurrent: boolean): string { function modelButtonLabel(m: ORModelListItem, isCurrent: boolean): string {
const free = isFreeModel(m); const free = isFreeModel(m);
const thinking = m.supported_parameters?.includes("reasoning") ?? false; 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. // is resolved at every callOpenRouterOnce so changes apply immediately.
bot.command("model", admin(async (ctx) => { await renderModelPicker(ctx); })); 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 // History — last N messages in this chat
bot.command("history", admin(async (ctx) => { bot.command("history", admin(async (ctx) => {
const n = Math.min(50, Math.max(5, Number.parseInt((ctx.match as string).trim(), 10) || 20)); const n = Math.min(50, Math.max(5, Number.parseInt((ctx.match as string).trim(), 10) || 20));