From de22f822e0f80c041d11bfaef2ca321f866f6cd1 Mon Sep 17 00:00:00 2001 From: Tyler Yust Date: Thu, 12 Mar 2026 17:34:12 -0700 Subject: [PATCH] fix: stop guessing session model costs --- ...s-writing-models-json-no-env-token.test.ts | 15 ++++++- src/agents/openclaw-tools.sessions.test.ts | 3 ++ src/agents/tools/sessions-helpers.ts | 1 + src/agents/tools/sessions-list-tool.ts | 2 + src/utils/usage-format.test.ts | 18 ++------ src/utils/usage-format.ts | 44 +------------------ 6 files changed, 25 insertions(+), 58 deletions(-) diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index ff38fe5e64a..8851d42cbd5 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -83,10 +83,23 @@ describe("models-config", () => { const modelPath = path.join(resolveOpenClawAgentDir(), "models.json"); const raw = await fs.readFile(modelPath, "utf8"); const parsed = JSON.parse(raw) as { - providers: Record; + providers: Record< + string, + { + baseUrl?: string; + models?: Array<{ + id?: string; + cost?: { input?: number; output?: number; cacheRead?: number; cacheWrite?: number }; + }>; + } + >; }; expect(parsed.providers["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1"); + expect(parsed.providers["custom-proxy"]?.models?.[0]).toMatchObject({ + id: "llama-3.1-8b", + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + }); }); }); diff --git a/src/agents/openclaw-tools.sessions.test.ts b/src/agents/openclaw-tools.sessions.test.ts index 7e92397e179..90f991b4484 100644 --- a/src/agents/openclaw-tools.sessions.test.ts +++ b/src/agents/openclaw-tools.sessions.test.ts @@ -123,6 +123,7 @@ describe("sessions tools", () => { status: "running", startedAt: 100, runtimeMs: 42, + estimatedCostUsd: 0.0042, childSessions: ["agent:main:subagent:worker"], }, { @@ -164,6 +165,7 @@ describe("sessions tools", () => { status?: string; startedAt?: number; runtimeMs?: number; + estimatedCostUsd?: number; childSessions?: string[]; messages?: Array<{ role?: string }>; }>; @@ -178,6 +180,7 @@ describe("sessions tools", () => { expect(group?.status).toBe("running"); expect(group?.startedAt).toBe(100); expect(group?.runtimeMs).toBe(42); + expect(group?.estimatedCostUsd).toBe(0.0042); expect(group?.childSessions).toEqual(["agent:main:subagent:worker"]); const cronOnly = await tool.execute("call2", { kinds: ["cron"] }); diff --git a/src/agents/tools/sessions-helpers.ts b/src/agents/tools/sessions-helpers.ts index 50132efb8b2..13bd8d2b169 100644 --- a/src/agents/tools/sessions-helpers.ts +++ b/src/agents/tools/sessions-helpers.ts @@ -58,6 +58,7 @@ export type SessionListRow = { model?: string; contextTokens?: number | null; totalTokens?: number | null; + estimatedCostUsd?: number; status?: SessionRunStatus; startedAt?: number; endedAt?: number; diff --git a/src/agents/tools/sessions-list-tool.ts b/src/agents/tools/sessions-list-tool.ts index 6423ed3ea42..86ace947ca7 100644 --- a/src/agents/tools/sessions-list-tool.ts +++ b/src/agents/tools/sessions-list-tool.ts @@ -203,6 +203,8 @@ export function createSessionsListTool(opts?: { model: typeof entry.model === "string" ? entry.model : undefined, contextTokens: typeof entry.contextTokens === "number" ? entry.contextTokens : undefined, totalTokens: typeof entry.totalTokens === "number" ? entry.totalTokens : undefined, + estimatedCostUsd: + typeof entry.estimatedCostUsd === "number" ? entry.estimatedCostUsd : undefined, status: typeof entry.status === "string" ? entry.status : undefined, startedAt: typeof entry.startedAt === "number" ? entry.startedAt : undefined, endedAt: typeof entry.endedAt === "number" ? entry.endedAt : undefined, diff --git a/src/utils/usage-format.test.ts b/src/utils/usage-format.test.ts index 90b8d182a42..c6926b99111 100644 --- a/src/utils/usage-format.test.ts +++ b/src/utils/usage-format.test.ts @@ -60,33 +60,23 @@ describe("usage-format", () => { expect(total).toBeCloseTo(0.003); }); - it("falls back to built in pricing for common models", () => { + it("returns undefined when model pricing is not configured", () => { expect( resolveModelCostConfig({ provider: "anthropic", model: "claude-sonnet-4-6", }), - ).toEqual({ - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }); + ).toBeUndefined(); expect( resolveModelCostConfig({ provider: "openai-codex", model: "gpt-5.4", }), - ).toEqual({ - input: 2, - output: 8, - cacheRead: 0, - cacheWrite: 0, - }); + ).toBeUndefined(); }); - it("prefers configured pricing over built in defaults", () => { + it("uses configured pricing when present", () => { const config = { models: { providers: { diff --git a/src/utils/usage-format.ts b/src/utils/usage-format.ts index 2839a034f36..1086163bf20 100644 --- a/src/utils/usage-format.ts +++ b/src/utils/usage-format.ts @@ -48,45 +48,6 @@ export function formatUsd(value?: number): string | undefined { return `$${value.toFixed(4)}`; } -const BUILTIN_MODEL_COSTS: Record = { - "anthropic/claude-opus-4-6": { - input: 15, - output: 75, - cacheRead: 1.5, - cacheWrite: 18.75, - }, - "anthropic/claude-sonnet-4-6": { - input: 3, - output: 15, - cacheRead: 0.3, - cacheWrite: 3.75, - }, - "anthropic/claude-haiku-4-5": { - input: 0.8, - output: 4, - cacheRead: 0.08, - cacheWrite: 1, - }, - "openai-codex/gpt-5.4": { - input: 2, - output: 8, - cacheRead: 0, - cacheWrite: 0, - }, - "openai-codex/gpt-5.3-codex": { - input: 1, - output: 4, - cacheRead: 0, - cacheWrite: 0, - }, - "openai-codex/gpt-5.3-codex-spark": { - input: 0.5, - output: 2, - cacheRead: 0, - cacheWrite: 0, - }, -}; - export function resolveModelCostConfig(params: { provider?: string; model?: string; @@ -99,10 +60,7 @@ export function resolveModelCostConfig(params: { } const providers = params.config?.models?.providers ?? {}; const entry = providers[provider]?.models?.find((item) => item.id === model); - if (entry?.cost) { - return entry.cost; - } - return BUILTIN_MODEL_COSTS[`${provider.toLowerCase()}/${model.toLowerCase()}`]; + return entry?.cost; } const toNumber = (value: number | undefined): number =>