diff --git a/packages/adapter-utils/src/types.ts b/packages/adapter-utils/src/types.ts index d065722def..b6d7439a5d 100644 --- a/packages/adapter-utils/src/types.ts +++ b/packages/adapter-utils/src/types.ts @@ -101,6 +101,12 @@ export interface AdapterExecutionResult { model?: string | null; billingType?: AdapterBillingType | null; costUsd?: number | null; + /** + * Provider-billed cost after prompt-cache discounts. Adapters should set + * this when they expose it separately; otherwise the server treats a + * provider-reported `costUsd` as the cache-adjusted billed amount. + */ + cacheAdjustedCostUsd?: number | null; resultJson?: Record | null; runtimeServices?: AdapterRuntimeServiceReport[]; /** diff --git a/server/src/__tests__/heartbeat-cost-accounting.test.ts b/server/src/__tests__/heartbeat-cost-accounting.test.ts index 9f55385d69..182b8acbb0 100644 --- a/server/src/__tests__/heartbeat-cost-accounting.test.ts +++ b/server/src/__tests__/heartbeat-cost-accounting.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { resolveLedgerCostStatus } from "../services/heartbeat.js"; +import { + resolveCacheAdjustedCostUsd, + resolveLedgerCostStatus, +} from "../services/heartbeat.js"; describe("heartbeat cost accounting", () => { it("marks token-bearing CLI usage without a reported cost as unpriced", () => { @@ -19,4 +22,46 @@ describe("heartbeat cost accounting", () => { outputTokens: 77_000, })).toBe("reported"); }); + + it("uses an explicit cache-adjusted provider cost when available", () => { + expect(resolveCacheAdjustedCostUsd({ + costUsd: 1.25, + cacheAdjustedCostUsd: 0.92, + })).toBe(0.92); + }); + + it("attributes provider-reported billed cost as cache-adjusted by default", () => { + expect(resolveCacheAdjustedCostUsd({ + costUsd: 1.25, + cacheAdjustedCostUsd: null, + })).toBe(1.25); + }); + + it("does not attribute invalid or unavailable costs", () => { + expect(resolveCacheAdjustedCostUsd({ + costUsd: null, + cacheAdjustedCostUsd: Number.NaN, + })).toBeNull(); + }); + + it("prices a run that only reports a cache-adjusted cost", () => { + const billedCostUsd = resolveCacheAdjustedCostUsd({ + costUsd: null, + cacheAdjustedCostUsd: 0.42, + }); + expect(billedCostUsd).toBe(0.42); + expect(resolveLedgerCostStatus({ + costUsd: billedCostUsd, + inputTokens: 1_000, + cachedInputTokens: 900_000, + outputTokens: 5_000, + })).toBe("reported"); + }); + + it("bills the discounted amount when both nominal and cache-adjusted costs are reported", () => { + expect(resolveCacheAdjustedCostUsd({ + costUsd: 3.1, + cacheAdjustedCostUsd: 1.5, + })).toBe(1.5); + }); }); diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index aed926c578..491d804a0e 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -3413,6 +3413,21 @@ export function resolveLedgerCostStatus(input: { return input.costUsd == null && hasTokenUsage ? "unpriced" : "reported"; } +export function resolveCacheAdjustedCostUsd(input: { + costUsd?: number | null; + cacheAdjustedCostUsd?: number | null; +}) { + const explicit = input.cacheAdjustedCostUsd; + if (typeof explicit === "number" && Number.isFinite(explicit) && explicit >= 0) { + return explicit; + } + const reported = input.costUsd; + if (typeof reported === "number" && Number.isFinite(reported) && reported >= 0) { + return reported; + } + return null; +} + export async function resolveLedgerScopeForRun( db: Db, companyId: string, @@ -12551,10 +12566,11 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) const outputTokens = usage?.outputTokens ?? 0; const cachedInputTokens = usage?.cachedInputTokens ?? 0; const billingType = normalizeLedgerBillingType(result.billingType); - const additionalCostCents = normalizeBilledCostCents(result.costUsd, billingType); + const billedCostUsd = resolveCacheAdjustedCostUsd(result); + const additionalCostCents = normalizeBilledCostCents(billedCostUsd, billingType); const hasTokenUsage = inputTokens > 0 || outputTokens > 0 || cachedInputTokens > 0; const costStatus = resolveLedgerCostStatus({ - costUsd: result.costUsd, + costUsd: billedCostUsd, inputTokens, cachedInputTokens, outputTokens, @@ -14809,8 +14825,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ? "timed_out" : "failed"; + const cacheAdjustedCostUsd = resolveCacheAdjustedCostUsd(adapterResult); const usageJson = - normalizedUsage || adapterResult.costUsd != null + normalizedUsage || adapterResult.costUsd != null || cacheAdjustedCostUsd != null ? ({ ...(normalizedUsage ?? {}), ...(rawUsage ? { @@ -14836,8 +14853,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) biller: resolveLedgerBiller(adapterResult), model: readNonEmptyString(adapterResult.model) ?? "unknown", ...(adapterResult.costUsd != null ? { costUsd: adapterResult.costUsd } : {}), + ...(cacheAdjustedCostUsd != null ? { cacheAdjustedCostUsd } : {}), costStatus: resolveLedgerCostStatus({ - costUsd: adapterResult.costUsd, + costUsd: cacheAdjustedCostUsd, inputTokens: normalizedUsage?.inputTokens ?? 0, cachedInputTokens: normalizedUsage?.cachedInputTokens ?? 0, outputTokens: normalizedUsage?.outputTokens ?? 0,