Every bot message gets an info button that opens its details: the provider and model behind it, tokens and cost, the job that ran it, each model used and every call in order. Each model call now records how it was made in token_usages.settings (generation settings sent, whose settings they were, pinned or automatic model, fallbacks), shown per model in the modal. Live replies now carry their token usage, from the job's push and the live relay alike, so it shows without a reload. Co-Authored-By: Claude Opus 5.5 <[email protected]>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import BunSQLite from "@moduletrace/bun-sqlite";
|
|
import type {
|
|
BUN_SQLITE_COBBLE_TOKEN_USAGES,
|
|
BunSQLiteTables,
|
|
} from "@/db/types/db";
|
|
import { logError } from "@/src/functions/backend/logger";
|
|
import { grabHarnessUsageScope } from "./harness-usage-scope";
|
|
import type { HarnessUsageCall, HarnessUsageScope } from "./types";
|
|
|
|
type Params = {
|
|
/**
|
|
* The scope the call ran under. Left out, the ambient one
|
|
* (`grabHarnessUsageScope`) is used, which is what every entry point
|
|
* does: the ids came in through `runWithHarnessUsageScope`.
|
|
*/
|
|
scope?: HarnessUsageScope;
|
|
/**
|
|
* Which harness and which model answered, with what the provider
|
|
* reported it cost.
|
|
*/
|
|
call: HarnessUsageCall;
|
|
};
|
|
|
|
/**
|
|
* Writes one model call into `token_usages`: the tokens it spent, on which
|
|
* model and provider, for whom, and which message or job it was for. This
|
|
* is what a message's token line, a bot's and a provider's totals and the
|
|
* usage page all read.
|
|
*
|
|
* Never throws: a model that answered must not have its answer lost
|
|
* because the bookkeeping failed. A call without an owner (no scope, no
|
|
* user) is not written at all, since nothing could show it.
|
|
*/
|
|
export default async function recordHarnessUsage({ scope, call }: Params) {
|
|
const resolved = scope || grabHarnessUsageScope();
|
|
const user_id = Number(resolved?.user_id) || null;
|
|
|
|
if (!user_id) return;
|
|
|
|
const row: BUN_SQLITE_COBBLE_TOKEN_USAGES = {
|
|
user_id,
|
|
bot_id: numberOrNull(resolved?.bot_id),
|
|
workspace_id: numberOrNull(resolved?.workspace_id),
|
|
session_id: numberOrNull(resolved?.session_id),
|
|
message_id: numberOrNull(resolved?.message_id),
|
|
job_id: numberOrNull(resolved?.job_id),
|
|
harness: call.harness,
|
|
provider_id: call.provider_id,
|
|
provider_name: call.provider_name,
|
|
model_id: call.model_id,
|
|
model_slug: call.model_slug,
|
|
prompt_tokens: Math.max(0, Math.round(call.prompt_tokens || 0)),
|
|
completion_tokens: Math.max(0, Math.round(call.completion_tokens || 0)),
|
|
cost: typeof call.cost == "number" && Number.isFinite(call.cost) ? call.cost : undefined,
|
|
settings: call.settings ? JSON.stringify(call.settings) : undefined,
|
|
};
|
|
|
|
try {
|
|
const res = await BunSQLite.insert<
|
|
BUN_SQLITE_COBBLE_TOKEN_USAGES,
|
|
(typeof BunSQLiteTables)[number]
|
|
>({
|
|
table: "token_usages",
|
|
data: [row],
|
|
});
|
|
|
|
if (!res.success) {
|
|
throw new Error(res.msg || `The usage wasn't saved.`);
|
|
}
|
|
} catch (error: unknown) {
|
|
await logError({
|
|
title: "Model usage not recorded",
|
|
error,
|
|
metadata: {
|
|
harness: call.harness,
|
|
model_slug: call.model_slug || null,
|
|
provider_name: call.provider_name || null,
|
|
user_id,
|
|
},
|
|
}).catch(() => {});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SQLite stores NULL for what isn't given; `Number(undefined)` is NaN,
|
|
* which bun-sqlite would refuse. An absent id is `undefined`, not `null`:
|
|
* bun-sqlite's row types spell "no value" that way.
|
|
*/
|
|
function numberOrNull(value: unknown): number | undefined {
|
|
const number = Number(value);
|
|
|
|
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
}
|