cache usage per ai account, so a switched-away account still answers
half a usage reading cannot be taken for an account you are not signed into. the limits come from anthropic's oauth endpoint using the credential the cli holds for the CURRENT login, and the tokens come from that login's transcripts — switch accounts and the previous one is not slow to read, it is unreadable. so: `ai_provider_usage`, keyed (user_id, ai_provider, email_account), with a snapshot taken at the end of every turn. fire-and-forget off the `result` message, owner-gated, capped by the reader's own 60s cache so a burst of short turns is not a burst of 150MB scans. the panel gains tabs, one per account. GET /chat/usage now answers from postgres alone — instant, and it carries the accounts that cannot be measured — and ?refresh=1 re-measures the signed-in one and writes it back. the client fires both and swaps. the reason a stale row is worth showing rather than apologising for is that `resetsAt` is an ABSOLUTE instant. a row from six hours ago has an as-of percentage and an exactly correct "resets in 14h 20m", ticked client-side with no network. and where a window has rolled over since, `currentLimit` reads it as 0% — the stored figure is not merely stale there, it is wrong in the direction that matters, since the account is actually free. weekly rolls forward to its next boundary (the length is in the kind, so that is arithmetic); session does not, because the five-hour window starts on first use rather than on a grid and there is no clock to count down. fetchedAt is now the older of the two halves rather than a stamp at the end, so a cached reading does not report itself as current. needs `bun db:push`. degrades to no tabs without it rather than a 500. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,8 @@ import { logger } from './logger';
|
||||
import { resolveHomeDir } from '@@/user-home';
|
||||
import type { ChatIdentity } from './claude-sessions';
|
||||
import { officerModelForApiModel } from './model-aliases';
|
||||
import { readClaudeUsage } from './claude-usage';
|
||||
import { readAccountEmail } from './claude-usage';
|
||||
import { listStoredUsage, mergeUsage, snapshotClaudeUsage } from './usage-store';
|
||||
import { readSttConfig } from '../server-settings/stt';
|
||||
|
||||
/**
|
||||
@@ -408,8 +409,27 @@ chatRouter.post('/tasks/:id/pause', async (ctx) => {
|
||||
*/
|
||||
chatRouter.get('/usage', async (ctx) => {
|
||||
const who = await chatIdentity(ctx.get('user'));
|
||||
const user = ctx.get('user');
|
||||
if (!who.isOwner) throw errors.FORBIDDEN('Usage belongs to the account that owns this machine');
|
||||
return ctx.json(await readClaudeUsage());
|
||||
|
||||
const currentEmail = await readAccountEmail();
|
||||
const stored = await listStoredUsage(user.id, currentEmail);
|
||||
|
||||
/*
|
||||
Two modes, and the split is the whole point of the cache.
|
||||
|
||||
Without `?refresh=1` this answers from Postgres alone — no transcript scan, no network — so the panel
|
||||
paints instantly with every account, including the ones that CANNOT be read live because the CLI is not
|
||||
signed in as them. The client then asks again with the flag and swaps in the fresh tab.
|
||||
|
||||
The exception is a first open, where answering instantly with nothing would be a worse kind of fast: if
|
||||
the signed-in account has no stored row, measure it now rather than show an empty panel.
|
||||
*/
|
||||
const refresh = ctx.req.query('refresh') === '1';
|
||||
const missing = !!currentEmail && !stored.some((a) => a.current);
|
||||
if (!refresh && !missing) return ctx.json({ accounts: stored });
|
||||
|
||||
return ctx.json({ accounts: mergeUsage(stored, await snapshotClaudeUsage(user.id)) });
|
||||
});
|
||||
|
||||
chatRouter.get('/models', async (ctx: Context) => {
|
||||
|
||||
Reference in New Issue
Block a user