mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
MUL-8050: price Cursor runtime catalog variants (#9295)
Co-authored-by: Cursor <[email protected]> Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
ffce31a122
commit
18fa9f728a
@@ -613,6 +613,68 @@ describe("estimateCost", () => {
|
||||
expect(costWithAllTokenTypes("cursor")).toBeCloseTo(3 + 15 + 0.5, 5);
|
||||
});
|
||||
|
||||
it("prices Cursor catalog variants at their base model's Cursor rate", () => {
|
||||
// Cursor ids carry effort / thinking parts the rate doesn't depend on.
|
||||
// 1M each of input, output, cache read, cache write.
|
||||
const cost = (model: string) =>
|
||||
estimateCost({
|
||||
...zeroUsage,
|
||||
provider: "cursor",
|
||||
model,
|
||||
input_tokens: 1_000_000,
|
||||
output_tokens: 1_000_000,
|
||||
cache_read_tokens: 1_000_000,
|
||||
cache_write_tokens: 1_000_000,
|
||||
});
|
||||
|
||||
const opus55 = 4 + 20 + 0.2 + 5;
|
||||
expect(cost("claude-opus-5-5-high")).toBeCloseTo(opus55, 5);
|
||||
expect(cost("claude-opus-5-5-max")).toBeCloseTo(opus55, 5);
|
||||
expect(cost("claude-fable-5-1-thinking-high")).toBeCloseTo(10 + 50 + 0.25 + 12.5, 5);
|
||||
// Thinking after the effort level, and the older `claude-4.6-sonnet` naming.
|
||||
expect(cost("claude-4.6-sonnet-medium-thinking")).toBeCloseTo(3 + 15 + 0.3 + 3.75, 5);
|
||||
expect(cost("gpt-5.5-extra-high")).toBeCloseTo(5 + 30 + 0.5, 5);
|
||||
expect(cost("gpt-5.6-sol-none")).toBeCloseTo(4 + 20 + 0.4 + 5, 5);
|
||||
expect(cost("cursor-grok-4.6-high")).toBeCloseTo(2 + 6 + 0.5, 5);
|
||||
expect(cost("grok-4.7-medium")).toBeCloseTo(2 + 6 + 0.5, 5);
|
||||
expect(cost("gemini-4-argon-medium")).toBeCloseTo(2 + 10 + 0.1, 5);
|
||||
expect(cost("muse-wonder-1.0-minimal")).toBeCloseTo(4 + 20 + 0.2, 5);
|
||||
expect(cost("kimi-k3-max")).toBeCloseTo(3 + 15 + 0.3, 5);
|
||||
expect(cost("claude-opus-4-8[context=1m,effort=high,fast=false]")).toBeCloseTo(
|
||||
5 + 25 + 0.5 + 6.25,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it("prices Cursor Fast variants only where Cursor publishes a Fast rate", () => {
|
||||
const cost = (model: string) =>
|
||||
estimateCost({
|
||||
...zeroUsage,
|
||||
provider: "cursor",
|
||||
model,
|
||||
input_tokens: 1_000_000,
|
||||
output_tokens: 1_000_000,
|
||||
});
|
||||
|
||||
expect(cost("gpt-5.6-sol-medium-fast")).toBeCloseTo(8 + 40, 5);
|
||||
expect(cost("cursor-grok-4.5-high-fast")).toBeCloseTo(4 + 18, 5);
|
||||
expect(cost("claude-opus-4-7-thinking-high-fast")).toBeCloseTo(30 + 150, 5);
|
||||
expect(cost("claude-opus-4-8[effort=high,fast=true]")).toBeCloseTo(10 + 50, 5);
|
||||
// No published Fast rate: unmapped, never the cheaper standard rate.
|
||||
expect(isModelPriced("claude-opus-5-5-high-fast", "cursor")).toBe(false);
|
||||
expect(isModelPriced("gpt-5.5-medium-fast", "cursor")).toBe(false);
|
||||
expect(isModelPriced("claude-opus-5-5[fast=true]", "cursor")).toBe(false);
|
||||
expect(collectUnmappedModels([
|
||||
{ ...zeroUsage, provider: "cursor", model: "claude-opus-5-5-high-fast", input_tokens: 1 },
|
||||
])).toEqual(["cursor/claude-opus-5-5-high-fast"]);
|
||||
});
|
||||
|
||||
it("keeps Cursor variant stripping scoped to Cursor rows", () => {
|
||||
expect(isModelPriced("claude-opus-5-5-high")).toBe(false);
|
||||
expect(isModelPriced("claude-opus-5-5-high", "claude")).toBe(false);
|
||||
expect(isModelPriced("claude-opus-5-5-high", "cursor")).toBe(true);
|
||||
});
|
||||
|
||||
it("scopes the generic `auto` id by provider so collisions don't borrow a price", () => {
|
||||
const auto = (provider?: string) =>
|
||||
estimateCost({ ...zeroUsage, provider, model: "auto", input_tokens: 1_000_000 });
|
||||
|
||||
@@ -382,7 +382,7 @@ const MODEL_PRICING: Record<
|
||||
"grok-4.20-0309-reasoning": { input: 1.25, output: 2.50, cacheRead: 0.20, cacheWrite: 1.25 },
|
||||
"grok-4.20-0309-non-reasoning": { input: 1.25, output: 2.50, cacheRead: 0.20, cacheWrite: 1.25 },
|
||||
|
||||
// -- Cursor Composer / Auto (cursor.com/docs/models-and-pricing,
|
||||
// -- Cursor models: Composer / Grok / Auto (cursor.com/docs/models-and-pricing,
|
||||
// cursor.com/docs/models/cursor-composer-2,
|
||||
// cursor.com/docs/models/cursor-composer-2-5).
|
||||
// Cursor's model ids are all unprefixed generic names (`auto`,
|
||||
@@ -400,6 +400,70 @@ const MODEL_PRICING: Record<
|
||||
"cursor/composer-2": { input: 0.5, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
|
||||
"cursor/composer-1.5": { input: 3.5, output: 17.5, cacheRead: 0.35, cacheWrite: 0 },
|
||||
"cursor/composer-1": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
|
||||
"cursor/grok-4.7": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"cursor/grok-4.7-fast": { input: 4, output: 12, cacheRead: 1, cacheWrite: 0 },
|
||||
"cursor/grok-4.6": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"cursor/grok-4.6-fast": { input: 4, output: 12, cacheRead: 1, cacheWrite: 0 },
|
||||
"cursor/grok-4.5": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"cursor/grok-4.5-fast": { input: 4, output: 18, cacheRead: 1, cacheWrite: 0 },
|
||||
|
||||
// -- Third-party models through Cursor (cursor.com/docs/models-and-pricing,
|
||||
// "Model pricing"). Cursor bills these at API list price, but its sheet
|
||||
// is the bill, so Cursor rows use it even where it differs from the
|
||||
// vendor rows above (GPT-5.6 Sol is on a Cursor promo through
|
||||
// 2026-11-21; update the row when it ends). Keys are the Cursor base id
|
||||
// left by `cursorBaseModel`, which drops the effort / thinking / fast
|
||||
// variant parts — effort and thinking don't change per-token rates.
|
||||
// A `-fast` row exists only where Cursor publishes the Fast rate; other
|
||||
// Fast variants stay unmapped rather than borrowing the standard rate.
|
||||
// Where the sheet lists no cache-write rate, cacheWrite is 0 (same as the
|
||||
// Composer rows). Long-context surcharges (GPT >272K input) can't be
|
||||
// seen in aggregated rows, so those requests are underestimated. --
|
||||
"cursor/claude-opus-5-5": { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 },
|
||||
"cursor/claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
"cursor/claude-opus-4-8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
// Published as "3x lower per-token pricing than Opus 4.7 fast mode".
|
||||
"cursor/claude-opus-4-8-fast": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
||||
"cursor/claude-opus-4-7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
"cursor/claude-opus-4-7-fast": { input: 30, output: 150, cacheRead: 3, cacheWrite: 37.5 },
|
||||
"cursor/claude-4.6-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
"cursor/claude-4.5-opus": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
||||
"cursor/claude-fable-5-1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
|
||||
"cursor/claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
|
||||
"cursor/claude-sonnet-5-5": { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 2.5 },
|
||||
"cursor/claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
|
||||
"cursor/claude-4.6-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
"cursor/claude-4.5-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
"cursor/claude-4-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
// <=100K prompt tier; see the claude-haiku-5-5 row above.
|
||||
"cursor/claude-haiku-5-5": { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 },
|
||||
"cursor/gpt-5.6-sol": { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 },
|
||||
"cursor/gpt-5.6-sol-fast": { input: 8, output: 40, cacheRead: 0.8, cacheWrite: 10 },
|
||||
"cursor/gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
|
||||
"cursor/gpt-5.6-terra-fast": { input: 4, output: 24, cacheRead: 0.4, cacheWrite: 5 },
|
||||
"cursor/gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
|
||||
"cursor/gpt-5.6-luna-fast": { input: 0.4, output: 2.4, cacheRead: 0.04, cacheWrite: 0.5 },
|
||||
"cursor/gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"cursor/gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
|
||||
"cursor/gpt-5.4-fast": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
|
||||
"cursor/gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
|
||||
"cursor/gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 },
|
||||
"cursor/gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
||||
"cursor/gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
|
||||
"cursor/gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
|
||||
"cursor/gemini-4-argon": { input: 2, output: 10, cacheRead: 0.1, cacheWrite: 0 },
|
||||
"cursor/gemini-3.8-flash": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 },
|
||||
"cursor/gemini-3.7-flash": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 },
|
||||
"cursor/gemini-3.6-flash": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"cursor/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0 },
|
||||
"cursor/gemini-3.1-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 },
|
||||
"cursor/gemini-3-flash": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0 },
|
||||
"cursor/kimi-k3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
|
||||
"cursor/kimi-k2.7-code": { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0 },
|
||||
"cursor/glm-5.2": { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0 },
|
||||
"cursor/muse-wonder-1.0": { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 0 },
|
||||
"cursor/muse-spark-1.3": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
|
||||
|
||||
// Legacy fallback bucket when neither the result event nor the runtime
|
||||
// model is known — the daemon emits the literal `cursor`. This key equals
|
||||
// the provider name itself, so it can't collide across providers and stays
|
||||
@@ -473,9 +537,44 @@ function pricingCandidates(model: string, provider?: string): string[] {
|
||||
const base = canonicalCandidates(model);
|
||||
const p = normalizeProvider(provider);
|
||||
if (!p) return base;
|
||||
if (p === "cursor") return cursorPricingCandidates(model, base);
|
||||
return [...base.map((c) => qualify(p, c)), ...base];
|
||||
}
|
||||
|
||||
// Cursor's catalog ids are a base model plus variant parts, in no fixed
|
||||
// order: `claude-opus-4-7-thinking-high-fast`, `claude-4.6-sonnet-medium-thinking`,
|
||||
// `cursor-grok-4.6-high`, or the parameterized
|
||||
// `claude-opus-4-8[context=1m,effort=high,fast=true]`.
|
||||
const CURSOR_VARIANT_SUFFIX = /-(?:fast|thinking|none|minimal|low|medium|high|xhigh|extra-high|max)$/;
|
||||
|
||||
function cursorBaseModel(model: string): { base: string; fast: boolean } {
|
||||
let id = model.trim().toLowerCase();
|
||||
let fast = false;
|
||||
const params = id.match(/\[([^\]]*)\]$/);
|
||||
if (params) {
|
||||
fast = /(?:^|,)\s*fast\s*=\s*true\s*(?:,|$)/.test(params[1] ?? "");
|
||||
id = id.slice(0, params.index);
|
||||
}
|
||||
for (let m = id.match(CURSOR_VARIANT_SUFFIX); m; m = id.match(CURSOR_VARIANT_SUFFIX)) {
|
||||
if (m[0] === "-fast") fast = true;
|
||||
id = id.slice(0, m.index);
|
||||
}
|
||||
return { base: id.replace(/^cursor-/, ""), fast };
|
||||
}
|
||||
|
||||
// Fast mode bills above the standard rate, so a Fast variant resolves only
|
||||
// through its exact id or its own `cursor/<base>-fast` row — never through a
|
||||
// canonical form (`[fast=true]` stripped as a context tag) or the base
|
||||
// model's price. Standard variants try the exact ids, then the Cursor base
|
||||
// row, then the bare vendor row for the same SKU.
|
||||
function cursorPricingCandidates(model: string, canonical: string[]): string[] {
|
||||
const { base, fast } = cursorBaseModel(model);
|
||||
if (fast) return [qualify("cursor", model), `cursor/${base}-fast`];
|
||||
const qualified = canonical.map((c) => qualify("cursor", c));
|
||||
if (!base) return [...qualified, ...canonical];
|
||||
return [...new Set([...qualified, `cursor/${base}`, base, ...canonical])];
|
||||
}
|
||||
|
||||
// The canonical storage/diagnostic key for a (model, provider) pair: the
|
||||
// provider-qualified form when a provider is known, else the bare model.
|
||||
// `collectUnmappedModels` returns these, and the custom-pricing dialog keys
|
||||
|
||||
Reference in New Issue
Block a user