mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 00:10:25 +00:00
Fix dashboard session cost metadata
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
archiveFileOnDisk,
|
||||
listSessionsFromStore,
|
||||
loadCombinedSessionStoreForGateway,
|
||||
loadGatewaySessionRow,
|
||||
loadSessionEntry,
|
||||
pruneLegacyStoreKeys,
|
||||
readSessionPreviewItemsFromTranscript,
|
||||
@@ -117,11 +118,26 @@ function emitSessionsChanged(
|
||||
if (connIds.size === 0) {
|
||||
return;
|
||||
}
|
||||
const sessionRow = payload.sessionKey ? loadGatewaySessionRow(payload.sessionKey) : null;
|
||||
context.broadcastToConnIds(
|
||||
"sessions.changed",
|
||||
{
|
||||
...payload,
|
||||
ts: Date.now(),
|
||||
...(sessionRow
|
||||
? {
|
||||
totalTokens: sessionRow.totalTokens,
|
||||
totalTokensFresh: sessionRow.totalTokensFresh,
|
||||
contextTokens: sessionRow.contextTokens,
|
||||
estimatedCostUsd: sessionRow.estimatedCostUsd,
|
||||
modelProvider: sessionRow.modelProvider,
|
||||
model: sessionRow.model,
|
||||
status: sessionRow.status,
|
||||
startedAt: sessionRow.startedAt,
|
||||
endedAt: sessionRow.endedAt,
|
||||
runtimeMs: sessionRow.runtimeMs,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
connIds,
|
||||
{ dropIfSlow: true },
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vit
|
||||
import { WebSocket } from "ws";
|
||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js";
|
||||
import { sessionsHandlers } from "./server-methods/sessions.js";
|
||||
import { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js";
|
||||
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
|
||||
import {
|
||||
@@ -447,6 +448,84 @@ describe("gateway server sessions", () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("sessions.changed mutation events include live usage metadata", async () => {
|
||||
const { dir } = await createSessionStoreDir();
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-main" }),
|
||||
JSON.stringify({
|
||||
id: "msg-usage-zero",
|
||||
message: {
|
||||
role: "assistant",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
usage: {
|
||||
input: 5_107,
|
||||
output: 1_827,
|
||||
cacheRead: 1_536,
|
||||
cacheWrite: 0,
|
||||
cost: { total: 0 },
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
}),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
await writeSessionStore({
|
||||
entries: {
|
||||
main: {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
contextTokens: 123_456,
|
||||
totalTokens: 0,
|
||||
totalTokensFresh: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const broadcastToConnIds = vi.fn();
|
||||
const respond = vi.fn();
|
||||
await sessionsHandlers["sessions.patch"]({
|
||||
params: {
|
||||
key: "main",
|
||||
label: "Renamed",
|
||||
},
|
||||
respond,
|
||||
context: {
|
||||
broadcastToConnIds,
|
||||
getSessionEventSubscriberConnIds: () => new Set(["conn-1"]),
|
||||
loadGatewayModelCatalog: async () => ({ providers: [] }),
|
||||
} as never,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({ ok: true, key: "agent:main:main" }),
|
||||
undefined,
|
||||
);
|
||||
expect(broadcastToConnIds).toHaveBeenCalledWith(
|
||||
"sessions.changed",
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:main",
|
||||
reason: "patch",
|
||||
totalTokens: 6_643,
|
||||
totalTokensFresh: true,
|
||||
contextTokens: 123_456,
|
||||
estimatedCostUsd: 0,
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
}),
|
||||
new Set(["conn-1"]),
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
});
|
||||
|
||||
test("lists and patches session store via sessions.* RPC", async () => {
|
||||
const { dir, storePath } = await createSessionStoreDir();
|
||||
const now = Date.now();
|
||||
|
||||
@@ -877,6 +877,99 @@ describe("listSessionsFromStore search", () => {
|
||||
expect(result.sessions[0]?.estimatedCostUsd).toBeCloseTo(0.007725, 8);
|
||||
});
|
||||
|
||||
test("keeps zero estimated session cost when configured model pricing resolves to free", () => {
|
||||
const cfg = {
|
||||
session: { mainKey: "main" },
|
||||
agents: { list: [{ id: "main", default: true }] },
|
||||
models: {
|
||||
providers: {
|
||||
"openai-codex": {
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.3-codex-spark",
|
||||
label: "GPT 5.3 Codex Spark",
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store: {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
inputTokens: 5_107,
|
||||
outputTokens: 1_827,
|
||||
cacheRead: 1_536,
|
||||
cacheWrite: 0,
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(result.sessions[0]?.estimatedCostUsd).toBe(0);
|
||||
});
|
||||
|
||||
test("falls back to transcript usage for totalTokens and zero estimatedCostUsd", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-utils-zero-cost-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, "sess-main.jsonl"),
|
||||
[
|
||||
JSON.stringify({ type: "session", version: 1, id: "sess-main" }),
|
||||
JSON.stringify({
|
||||
message: {
|
||||
role: "assistant",
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
usage: {
|
||||
input: 5_107,
|
||||
output: 1_827,
|
||||
cacheRead: 1_536,
|
||||
cost: { total: 0 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
try {
|
||||
const result = listSessionsFromStore({
|
||||
cfg: baseCfg,
|
||||
storePath,
|
||||
store: {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex-spark",
|
||||
totalTokens: 0,
|
||||
totalTokensFresh: false,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
} as SessionEntry,
|
||||
},
|
||||
opts: {},
|
||||
});
|
||||
|
||||
expect(result.sessions[0]?.totalTokens).toBe(6_643);
|
||||
expect(result.sessions[0]?.totalTokensFresh).toBe(true);
|
||||
expect(result.sessions[0]?.estimatedCostUsd).toBe(0);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("falls back to transcript usage for totalTokens and estimatedCostUsd, and derives contextTokens from the resolved model", () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-utils-"));
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
|
||||
@@ -226,6 +226,10 @@ function resolvePositiveNumber(value: number | null | undefined): number | undef
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveNonNegativeNumber(value: number | null | undefined): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveEstimatedSessionCostUsd(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider?: string;
|
||||
@@ -233,7 +237,7 @@ function resolveEstimatedSessionCostUsd(params: {
|
||||
entry?: Pick<SessionEntry, "inputTokens" | "outputTokens" | "cacheRead" | "cacheWrite">;
|
||||
explicitCostUsd?: number;
|
||||
}): number | undefined {
|
||||
const explicitCostUsd = resolvePositiveNumber(params.explicitCostUsd);
|
||||
const explicitCostUsd = resolveNonNegativeNumber(params.explicitCostUsd);
|
||||
if (explicitCostUsd !== undefined) {
|
||||
return explicitCostUsd;
|
||||
}
|
||||
@@ -266,7 +270,7 @@ function resolveEstimatedSessionCostUsd(params: {
|
||||
},
|
||||
cost,
|
||||
});
|
||||
return resolvePositiveNumber(estimated);
|
||||
return resolveNonNegativeNumber(estimated);
|
||||
}
|
||||
|
||||
function resolveChildSessionKeys(
|
||||
@@ -1085,7 +1089,7 @@ export function buildGatewaySessionRow(params: {
|
||||
provider: modelProvider,
|
||||
model,
|
||||
entry,
|
||||
}) ?? resolvePositiveNumber(transcriptUsage?.estimatedCostUsd);
|
||||
}) ?? resolveNonNegativeNumber(transcriptUsage?.estimatedCostUsd);
|
||||
const contextTokens =
|
||||
resolvePositiveNumber(entry?.contextTokens) ??
|
||||
resolvePositiveNumber(transcriptUsage?.contextTokens) ??
|
||||
|
||||
Reference in New Issue
Block a user