124 lines · 4.6 KB · TypeScript
RawDownload
import { Hono } from "hono"import { describeRoute, resolver } from "hono-openapi"import z from "zod"import { Session } from "../../../session"import { lazy } from "../../../util/lazy"import { Log } from "../../../util/log" const log = Log.create({ service: "settings-usage" }) const Tokens = z.object({  input: z.number(),  output: z.number(),  reasoning: z.number(),  cache_read: z.number(),  cache_write: z.number(),}) const Summary = z.object({  sessions: z.number(),  total: z.object({ cost: z.number(), tokens: Tokens }),  latest: z.object({ id: z.string(), title: z.string(), cost: z.number(), tokens: Tokens }).nullable(),  weekly: z.array(z.object({ date: z.string(), cost: z.number(), tokens: z.number() })),  by_model: z.array(    z.object({ key: z.string(), provider: z.string(), model: z.string(), cost: z.number(), tokens: z.number() }),  ),}) const emptyTokens = () => ({ input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }) function dayKey(ms: number): string {  const d = new Date(ms)  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`} // Real, locally-computed usage: sums assistant-message cost + token counts across// every session in the current project, grouped by model and by day. This is the// "where tokens go" ground truth — no estimates, just what the sessions recorded.export const SettingsUsageRoutes = lazy(() =>  new Hono().get(    "/",    describeRoute({      summary: "Local usage summary",      operationId: "settings.usage.get",      responses: {        200: {          description: "Usage summary",          content: { "application/json": { schema: resolver(Summary) } },        },      },    }),    async (c) => {      const total = { cost: 0, tokens: emptyTokens() }      const byModel = new Map<string, { provider: string; model: string; cost: number; tokens: number }>()       // Last 7 calendar days (oldest → newest), pre-seeded so gaps render as zero.      const now = Date.now()      const day = 24 * 60 * 60 * 1000      const weekly = new Map<string, { cost: number; tokens: number }>()      for (let i = 6; i >= 0; i--) weekly.set(dayKey(now - i * day), { cost: 0, tokens: 0 })      const weekStart = now - 7 * day       let latest: z.infer<typeof Summary>["latest"] = null      let latestUpdated = -1      let sessions = 0       try {        for await (const session of Session.list()) {          sessions++          const acc = { cost: 0, tokens: emptyTokens() }          const msgs = await Session.messages({ sessionID: session.id }).catch(() => [])          for (const { info } of msgs) {            if (info.role !== "assistant") continue            const t = info.tokens            const tokenCount = t.input + t.output + t.reasoning + t.cache.read + t.cache.write            total.cost += info.cost            total.tokens.input += t.input            total.tokens.output += t.output            total.tokens.reasoning += t.reasoning            total.tokens.cache_read += t.cache.read            total.tokens.cache_write += t.cache.write            acc.cost += info.cost            acc.tokens.input += t.input            acc.tokens.output += t.output            acc.tokens.reasoning += t.reasoning            acc.tokens.cache_read += t.cache.read            acc.tokens.cache_write += t.cache.write             const key = `${info.providerID}/${info.modelID}`            const row = byModel.get(key) ?? { provider: info.providerID, model: info.modelID, cost: 0, tokens: 0 }            row.cost += info.cost            row.tokens += tokenCount            byModel.set(key, row)             if (info.time.created >= weekStart) {              const bucket = weekly.get(dayKey(info.time.created))              if (bucket) {                bucket.cost += info.cost                bucket.tokens += tokenCount              }            }          }           const updated = session.time.updated ?? session.time.created          if (updated > latestUpdated) {            latestUpdated = updated            latest = { id: session.id, title: session.title || "Untitled", cost: acc.cost, tokens: acc.tokens }          }        }      } catch (e) {        log.warn("usage aggregation failed", { error: e instanceof Error ? e.message : String(e) })      }       return c.json({        sessions,        total,        latest,        weekly: [...weekly.entries()].map(([date, v]) => ({ date, cost: v.cost, tokens: v.tokens })),        by_model: [...byModel.entries()]          .map(([key, v]) => ({ key, ...v }))          .sort((a, b) => b.cost - a.cost || b.tokens - a.tokens),      })    },  ),)