From 067af135028f365b09e259830b3790e2ad898610 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Thu, 12 Mar 2026 01:50:15 -0700 Subject: [PATCH] Estimate session costs in sessions list --- src/gateway/session-utils.test.ts | 40 ++++++++++++++++++++++++++++++ src/gateway/session-utils.ts | 34 +++++++++++++++++++++++++ src/gateway/session-utils.types.ts | 1 + 3 files changed, 75 insertions(+) diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 1e5f700e01b..725dccae2f2 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -836,6 +836,46 @@ describe("listSessionsFromStore search", () => { expect(missing?.totalTokens).toBeUndefined(); expect(missing?.totalTokensFresh).toBe(false); }); + + test("includes estimated session cost when model pricing is configured", () => { + const cfg = { + session: { mainKey: "main" }, + agents: { list: [{ id: "main", default: true }] }, + models: { + providers: { + openai: { + models: [ + { + id: "gpt-5.4", + label: "GPT 5.4", + baseUrl: "https://api.openai.com/v1", + cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0.5 }, + }, + ], + }, + }, + }, + } as unknown as OpenClawConfig; + const result = listSessionsFromStore({ + cfg, + storePath: "/tmp/sessions.json", + store: { + "agent:main:main": { + sessionId: "sess-main", + updatedAt: Date.now(), + modelProvider: "openai", + model: "gpt-5.4", + inputTokens: 2_000, + outputTokens: 500, + cacheRead: 1_000, + cacheWrite: 200, + } as SessionEntry, + }, + opts: {}, + }); + + expect(result.sessions[0]?.estimatedCostUsd).toBeCloseTo(0.007725, 8); + }); }); describe("listSessionsFromStore subagent metadata", () => { diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 3a7ef894ef0..62816a1a778 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -44,6 +44,7 @@ import { resolveAvatarMime, } from "../shared/avatar-policy.js"; import { normalizeSessionDeliveryFields } from "../utils/delivery-context.js"; +import { estimateUsageCost, resolveModelCostConfig } from "../utils/usage-format.js"; import { readSessionTitleFieldsFromTranscript } from "./session-utils.fs.js"; import type { GatewayAgentRow, @@ -213,6 +214,32 @@ function resolveSessionRuntimeMs( return Math.max(0, (run.endedAt ?? now) - run.startedAt); } +function resolveEstimatedSessionCostUsd(params: { + cfg: OpenClawConfig; + provider?: string; + model?: string; + entry?: SessionEntry; +}): number | undefined { + const cost = resolveModelCostConfig({ + provider: params.provider, + model: params.model, + config: params.cfg, + }); + if (!cost) { + return undefined; + } + const estimated = estimateUsageCost({ + usage: { + input: params.entry?.inputTokens, + output: params.entry?.outputTokens, + cacheRead: params.entry?.cacheRead, + cacheWrite: params.entry?.cacheWrite, + }, + cost, + }); + return typeof estimated === "number" && Number.isFinite(estimated) ? estimated : undefined; +} + function resolveChildSessionKeys(controllerSessionKey: string): string[] | undefined { const childSessions = Array.from( new Set( @@ -960,6 +987,12 @@ export function listSessionsFromStore(params: { const model = resolvedModel.model ?? DEFAULT_MODEL; const subagentRun = getSubagentRunByChildSessionKey(key); const childSessions = resolveChildSessionKeys(key); + const estimatedCostUsd = resolveEstimatedSessionCostUsd({ + cfg, + provider: modelProvider, + model, + entry, + }); return { key, spawnedBy: entry?.spawnedBy, @@ -987,6 +1020,7 @@ export function listSessionsFromStore(params: { outputTokens: entry?.outputTokens, totalTokens: total, totalTokensFresh, + estimatedCostUsd, status: resolveSessionRunStatus(subagentRun), startedAt: subagentRun?.startedAt, endedAt: subagentRun?.endedAt, diff --git a/src/gateway/session-utils.types.ts b/src/gateway/session-utils.types.ts index bee9c3da320..17d2bd334c1 100644 --- a/src/gateway/session-utils.types.ts +++ b/src/gateway/session-utils.types.ts @@ -43,6 +43,7 @@ export type GatewaySessionRow = { outputTokens?: number; totalTokens?: number; totalTokensFresh?: boolean; + estimatedCostUsd?: number; status?: SessionRunStatus; startedAt?: number; endedAt?: number;