authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-09-25 19:27:37-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-25 19:27:37-05:00
logaade3a69a8536be98d2fe9b0fb9193289b70e90b
treea7cdaa644d226f18d0fae9b80fe8b3b51f2720ab
parent525af2d1ad4b6de847ea658c37a081fd7afe8ef6
signature Signed by PGP key B5690EEEBB952194

fix(usage): price Cursor cache savings by base model (#13731)


4 files changed, 46 insertions(+), 3 deletions(-)

apps/server/src/usage/cursorUsageReader.ts+13
......@@ -17,6 +17,18 @@ function tokens(value: unknown): number {
1717 return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
1818}
1919
20/**
21 * Maps Cursor's tiered names (`cursor-grok-4.6-high-fast`,
22 * `claude-fable-5-1-thinking-high`) to the base model's rate-table key.
23 * Grok resolves through xAI's first-party entry, which has no bare alias.
24 */
25export function cursorRateModel(model: string): string {
26 const base = model
27 .replace(/^cursor-/, "")
28 .replace(/(?:-thinking)?(?:-(?:none|minimal|low|medium|high|xhigh|max))?(?:-fast)?$/, "");
29 return base.startsWith("grok-") ? `xai/${base}` : base;
30}
31
2032export interface CursorAccountUsageReadResult {
2133 readonly accountKey: string | null;
2234 readonly records: readonly UsageRecord[];
......@@ -232,6 +244,7 @@ export async function readCursorAccountUsage(
232244 provider: "cursor",
233245 timestampMs,
234246 model: event.model,
247 rateModel: cursorRateModel(event.model),
235248 sessionId,
236249 totals,
237250 reportedCostUsd,
apps/server/src/usage/usagePricing.test.ts+21
......@@ -1,5 +1,6 @@
11import { describe, expect, it } from "@effect/vitest";
22
3import { cursorRateModel } from "./cursorUsageReader.ts";
34import {
45 cacheSavingsUsd,
56 createOverrideRateTable,
......@@ -49,6 +50,26 @@ describe("usage pricing", () => {
4950 expect(cacheSavingsUsd(table, record("example-model"), overrides)).toBe(1.5);
5051 });
5152
53 it("prices Cursor cache savings at the base model rate", () => {
54 const table = parseRateTable({
55 "claude-fable-5-1": rate(10e-6, 1e-6),
56 "xai/grok-4.7": rate(2e-6, 0.5e-6),
57 "openrouter/x-ai/grok-4.7": rate(3e-6, 0.5e-6),
58 });
59 const cursorRecord = (model: string) => ({
60 ...record(model, 0.25),
61 rateModel: cursorRateModel(model),
62 });
63
64 expect(cacheSavingsUsd(table, cursorRecord("claude-fable-5-1-thinking-high"))).toBeCloseTo(9);
65 expect(cacheSavingsUsd(table, cursorRecord("cursor-grok-4.7-high-fast"))).toBeCloseTo(1.5);
66 expect(cacheSavingsUsd(table, cursorRecord("default"))).toBe(0);
67 expect(priceUsage(table, cursorRecord("grok-4.7-xhigh-fast"))).toEqual({
68 costUsd: 0.25,
69 costSource: "providerReported",
70 });
71 });
72
5273 it("prices unknown models offline and uses input prices for omitted cache rates", () => {
5374 const table = parseRateTable({});
5475 const overrides = createOverrideRateTable({
apps/server/src/usage/usagePricing.ts+7-3
......@@ -184,7 +184,10 @@ export function lookupRate(table: RateTable, model: string): ModelRate | null {
184184}
185185
186186/** The parts of a transcript record that decide its price. */
187export type PricedRecord = Pick<UsageRecord, "model" | "totals" | "fast" | "reportedCostUsd">;
187export type PricedRecord = Pick<
188 UsageRecord,
189 "model" | "rateModel" | "totals" | "fast" | "reportedCostUsd"
190>;
188191
189192export interface PricedUsage {
190193 readonly costUsd: number;
......@@ -208,7 +211,7 @@ export function priceUsage(
208211 return { costUsd: reportedCostUsd, costSource: "providerReported" };
209212 }
210213
211 const rate = override ?? lookupRate(table, model);
214 const rate = override ?? lookupRate(table, record.rateModel ?? model);
212215 if (rate === null) return { costUsd: 0, costSource: "unpriced" };
213216
214217 const standardCostUsd =
......@@ -232,7 +235,8 @@ export function cacheSavingsUsd(
232235 record: PricedRecord,
233236 overrides?: RateTable,
234237): number {
235 const rate = overrides?.get(record.model.trim()) ?? lookupRate(table, record.model);
238 const rate =
239 overrides?.get(record.model.trim()) ?? lookupRate(table, record.rateModel ?? record.model);
236240 if (rate === null) return 0;
237241 return (
238242 record.totals.cachedInputTokens *
apps/server/src/usage/usageTranscripts.ts+5
......@@ -12,6 +12,11 @@ export interface UsageRecord {
1212 readonly provider: UsageProviderKind;
1313 readonly timestampMs: number;
1414 readonly model: string;
15 /**
16 * Rate-table key when the provider's display name carries tiers the table
17 * does not know, such as Cursor's `claude-opus-5-5-high`. Defaults to `model`.
18 */
19 readonly rateModel?: string;
1520 readonly sessionId: string;
1621 readonly totals: UsageTokenTotals;
1722 readonly reportedCostUsd: number | null;