mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai): add totalFromProvider flag to usage cost (#11178)
Add Usage.cost.totalFromProvider to record whether cost.total is the billed cost reported by the provider or an estimate from catalog rates. Catalog-derived totals set it to false; a missing value also means false. combineUsage marks a sum as provider-reported only when both parts are.
This commit is contained in:
parent
58c0b02ce5
commit
7c22a93d0e
8 changed files with 52 additions and 1 deletions
|
|
@ -233,6 +233,8 @@ for (const block of response.content) {
|
|||
}
|
||||
```
|
||||
|
||||
`usage.cost.totalFromProvider` is `true` when `usage.cost.total` is the billed cost reported by the provider and `false` when it is estimated from the model's catalog rates. A missing value, as in sessions recorded before this field existed, also means `false`.
|
||||
|
||||
Snippets in the rest of this README assume a `models` collection set up like this (with the relevant providers registered).
|
||||
|
||||
## Providers and Models
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ function parseUsage(
|
|||
cacheRead: (model.cost.cacheRead / 1000000) * cacheReadTokens,
|
||||
cacheWrite: (model.cost.cacheWrite / 1000000) * cacheWriteTokens,
|
||||
total: 0,
|
||||
totalFromProvider: false,
|
||||
},
|
||||
};
|
||||
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
||||
|
|
|
|||
|
|
@ -1252,6 +1252,7 @@ export function calculateCost(model: AnyModel, usage: Usage): Usage["cost"] {
|
|||
usage.cost.cacheRead = (rates.cacheRead / 1000000) * usage.cacheRead;
|
||||
usage.cost.cacheWrite = (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1000000;
|
||||
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
||||
usage.cost.totalFromProvider = false;
|
||||
return usage.cost;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -476,6 +476,8 @@ export interface Usage {
|
|||
cacheRead: number;
|
||||
cacheWrite: number;
|
||||
total: number;
|
||||
/** True when `total` is the billed cost reported by the provider. False when it is a catalog estimate; absent also means false. */
|
||||
totalFromProvider?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,19 @@ describe("Models runtime", () => {
|
|||
expect(long.cacheWrite).toBe(0.0000125);
|
||||
});
|
||||
|
||||
it("marks catalog-derived totals as not provider-reported", () => {
|
||||
const usage: Usage = {
|
||||
input: 1_000_000,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 1_000_000,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 9, totalFromProvider: true },
|
||||
};
|
||||
calculateCost(testModel("openai", "gpt-5.6-sol"), usage);
|
||||
expect(usage.cost.totalFromProvider).toBe(false);
|
||||
});
|
||||
|
||||
it("registers, replaces, and deletes providers", () => {
|
||||
const models = createModels();
|
||||
models.setProvider(testProvider({ id: "p1" }));
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ export function combineUsage(first: Usage, second: Usage): Usage {
|
|||
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
||||
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
||||
total: first.cost.total + second.cost.total,
|
||||
// A sum is provider-reported only when every part is.
|
||||
totalFromProvider: first.cost.totalFromProvider === true && second.cost.totalFromProvider === true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ describe("generateSummary reasoning options", () => {
|
|||
input: 20,
|
||||
output: 20,
|
||||
totalTokens: 40,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, totalFromProvider: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
30
packages/coding-agent/test/usage-totals.test.ts
Normal file
30
packages/coding-agent/test/usage-totals.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Usage } from "@earendil-works/pi-ai/compat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { combineUsage } from "../src/core/usage-totals.ts";
|
||||
|
||||
function usage(total: number, totalFromProvider?: boolean): Usage {
|
||||
return {
|
||||
input: 1,
|
||||
output: 1,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 2,
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
total,
|
||||
...(totalFromProvider === undefined ? {} : { totalFromProvider }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("combineUsage", () => {
|
||||
it("marks the combined total as provider-reported only when both totals are", () => {
|
||||
expect(combineUsage(usage(1, true), usage(2, true)).cost).toMatchObject({ total: 3, totalFromProvider: true });
|
||||
expect(combineUsage(usage(1, true), usage(2)).cost.totalFromProvider).toBe(false);
|
||||
expect(combineUsage(usage(1), usage(2, true)).cost.totalFromProvider).toBe(false);
|
||||
expect(combineUsage(usage(1), usage(2)).cost.totalFromProvider).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue