| 1 | /** |
| 2 | * What the proxy tells billing about one answer: its tokens, under the |
| 3 | * run's session and the person it is for, for usage views. Billing still |
| 4 | * prices runs from AI Gateway, not from these. |
| 5 | */ |
| 6 | import type { ModelUpstream } from "@g1t/contracts"; |
| 7 | |
| 8 | import type { Tokens } from "./usage"; |
| 9 | |
| 10 | export type TokenReport = { |
| 11 | workspace: string; |
| 12 | session: string; |
| 13 | person: string | null; |
| 14 | model: string; |
| 15 | tier: "small" | "large" | null; |
| 16 | input: number; |
| 17 | output: number; |
| 18 | cacheRead: number; |
| 19 | cacheWrite: number; |
| 20 | }; |
| 21 | |
| 22 | /** |
| 23 | * Whether a request's answer is a model's answer, and so used tokens. |
| 24 | * Counting tokens is a question about a request, not an answer. |
| 25 | */ |
| 26 | export function isAnswer(path: string): boolean { |
| 27 | return /^\/v1\/messages\/?(\?|$)/.test(path); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * The report for one answer, or null when it used nothing or its session |
| 32 | * has no id to count it under. The model is the run's when its route names |
| 33 | * one, else the one that answered. |
| 34 | */ |
| 35 | export function tokenReport(upstream: ModelUpstream, answeredBy: string | null, tokens: Tokens): TokenReport | null { |
| 36 | const used = tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite; |
| 37 | if (used === 0 || !upstream.session) return null; |
| 38 | return { |
| 39 | workspace: upstream.workspace, |
| 40 | session: upstream.session, |
| 41 | person: upstream.requestedBy ?? null, |
| 42 | model: upstream.model ?? answeredBy ?? "unknown", |
| 43 | tier: upstream.tier ?? null, |
| 44 | input: tokens.input, |
| 45 | output: tokens.output, |
| 46 | cacheRead: tokens.cacheRead, |
| 47 | cacheWrite: tokens.cacheWrite, |
| 48 | }; |
| 49 | } |